acttrader-charts 1.0.18 → 1.0.19

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
@@ -177,6 +177,8 @@ chart.setLevels([], 'label', 'price', 'pending'); // clear all of one type
177
177
 
178
178
  **Brackets follow entry on drag:** when the entry line of a pending order, draft order, or an open position with `entryPriceEditable: true` is dragged, any existing `stopLossPrice` / `takeProfitPrice` brackets translate along with it by the same price delta. The distance between entry and each bracket is whatever the user currently sees — if they manually drag SL or TP to a new price, that new distance becomes the anchor for the next entry drag. Missing brackets aren't auto-created. On confirm, `tradeLevelEdit` carries all translated fields together in one `changes[]` array; on mobile (`hideLevelConfirmCancel`) the three changes are applied as one atomic event.
179
179
 
180
+ **Bracket pill auto-offset:** when an SL or TP price sits within about one pill-height of the entry price, the bracket's label pill is pushed vertically away from the entry pill and connected back to its real price line by a dashed leader. The horizontal bracket line itself stays at the true price; only the pill and its `×` button move, and drag targets follow the displaced pill — so grabbing the pill always drags the bracket, grabbing the entry pill always drags the entry, and grabbing the real bracket line still drags the bracket. Works for both buy and sell orders without configuration.
181
+
180
182
  **TFC events:**
181
183
 
182
184
  ```ts
@@ -204,6 +206,14 @@ chart.on('tradeLevelClose', ({ label, type, action, isFullscreen }) => {
204
206
  chart.on('tradeLevelDrag', ({ label, newPrice, bracketType, isFullscreen }) => {
205
207
  updatePricePreview(label, newPrice);
206
208
  });
209
+
210
+ // Live qty while the user edits the QTY pill flyout — fires before the level edit is
211
+ // confirmed, so host-side Estimated PNL for SL/TP brackets can recompute immediately
212
+ // instead of waiting for `tradeLevelEdit` at ✓ confirm. `type` is `'draft'` for draft
213
+ // orders, otherwise the parent level's type.
214
+ chart.on('tradeLevelQtyChange', ({ label, type, newLots, previousLots }) => {
215
+ updateEstimatedPnl(label, newLots);
216
+ });
207
217
  ```
208
218
 
209
219
  **Off-viewport level indicators:**
@@ -784,8 +794,10 @@ chart.on('stateChange', ({ symbol, state }) => {
784
794
  chart.on('tradeLevelEdit', ({ label, type, data, changes, isFullscreen }) => {});
785
795
  chart.on('tradeLevelClose', ({ label, type, action, data, isFullscreen }) => {});
786
796
  chart.on('tradeLevelDrag', ({ label, newPrice, data, bracketType, isFullscreen }) => {}); // fires on every mouse-move during drag
797
+ chart.on('tradeLevelQtyChange', ({ label, type, newLots, previousLots, isFullscreen }) => {}); // live qty edit via QTY pill — use to refresh Estimated PNL on SL/TP brackets
787
798
  chart.on('tradeLevelEditOpen', ({ label, type, price, side, stopLossPrice, takeProfitPrice, data, isFullscreen }) => {});
788
799
  chart.on('tradeLevelConfirmed',({ label, type, isFullscreen }) => {});
800
+ chart.on('tradeLevelEditCancelled', ({ label, type, isFullscreen }) => {}); // ESC / inline ✕ aborted an edit — reset external modify-order panel
789
801
  chart.on('draftInitiated', ({ side, price, orderType, isFullscreen }) => {}); // new draft order shown — open buy/sell form
790
802
  chart.on('draftCancelled', ({ label, isFullscreen }) => {}); // draft order dismissed without confirming
791
803
  chart.on('tfcToggle', ({ enabled }) => {}); // TFC toggled on or off via top bar button or setTfcActive()
@@ -1126,6 +1126,14 @@ type ChartEventMap = {
1126
1126
  type: TradeLevelType | 'draft';
1127
1127
  isFullscreen: boolean;
1128
1128
  };
1129
+ /** Emitted when an in-progress level edit is cancelled from the chart (ESC key or inline ✕ cancel button).
1130
+ * Mirrors `tradeLevelConfirmed` but for the revert path. Not fired for draft orders (those emit `draftCancelled`).
1131
+ * External panels listen to reset their modify-order form when the user aborts the edit from the chart side. */
1132
+ tradeLevelEditCancelled: {
1133
+ label: string;
1134
+ type: TradeLevelType;
1135
+ isFullscreen: boolean;
1136
+ };
1129
1137
  /** Emitted when a draft order is cancelled (Escape, ✕ button, or external revert). */
1130
1138
  draftCancelled: {
1131
1139
  label: string;
@@ -1173,6 +1181,19 @@ type ChartEventMap = {
1173
1181
  uiStateChange: {
1174
1182
  hasOpenUI: boolean;
1175
1183
  };
1184
+ /** Emitted live while the user edits lot qty via the QTY pill flyout, before the
1185
+ * level edit is confirmed. Use this to recompute qty-derived UI (e.g. Estimated
1186
+ * PNL on SL/TP brackets) in real time. Final committed qty still arrives on
1187
+ * `tradeLevelEdit.newLots` at ✓ confirm (or immediately for draft orders).
1188
+ * `type` is `'draft'` for the in-progress draft order, otherwise the parent level's type.
1189
+ * `previousLots` is the qty at edit-session start (useful for revert-aware previews). */
1190
+ tradeLevelQtyChange: {
1191
+ label: string;
1192
+ type: TradeLevelType | 'draft';
1193
+ newLots: number;
1194
+ previousLots: number;
1195
+ isFullscreen: boolean;
1196
+ };
1176
1197
  /** Emitted when the user confirms all edits to a level — replaces separate tradeLevelDragEnd / tradeLevelBracketDrag events. */
1177
1198
  tradeLevelEdit: {
1178
1199
  label: string;
@@ -1126,6 +1126,14 @@ type ChartEventMap = {
1126
1126
  type: TradeLevelType | 'draft';
1127
1127
  isFullscreen: boolean;
1128
1128
  };
1129
+ /** Emitted when an in-progress level edit is cancelled from the chart (ESC key or inline ✕ cancel button).
1130
+ * Mirrors `tradeLevelConfirmed` but for the revert path. Not fired for draft orders (those emit `draftCancelled`).
1131
+ * External panels listen to reset their modify-order form when the user aborts the edit from the chart side. */
1132
+ tradeLevelEditCancelled: {
1133
+ label: string;
1134
+ type: TradeLevelType;
1135
+ isFullscreen: boolean;
1136
+ };
1129
1137
  /** Emitted when a draft order is cancelled (Escape, ✕ button, or external revert). */
1130
1138
  draftCancelled: {
1131
1139
  label: string;
@@ -1173,6 +1181,19 @@ type ChartEventMap = {
1173
1181
  uiStateChange: {
1174
1182
  hasOpenUI: boolean;
1175
1183
  };
1184
+ /** Emitted live while the user edits lot qty via the QTY pill flyout, before the
1185
+ * level edit is confirmed. Use this to recompute qty-derived UI (e.g. Estimated
1186
+ * PNL on SL/TP brackets) in real time. Final committed qty still arrives on
1187
+ * `tradeLevelEdit.newLots` at ✓ confirm (or immediately for draft orders).
1188
+ * `type` is `'draft'` for the in-progress draft order, otherwise the parent level's type.
1189
+ * `previousLots` is the qty at edit-session start (useful for revert-aware previews). */
1190
+ tradeLevelQtyChange: {
1191
+ label: string;
1192
+ type: TradeLevelType | 'draft';
1193
+ newLots: number;
1194
+ previousLots: number;
1195
+ isFullscreen: boolean;
1196
+ };
1176
1197
  /** Emitted when the user confirms all edits to a level — replaces separate tradeLevelDragEnd / tradeLevelBracketDrag events. */
1177
1198
  tradeLevelEdit: {
1178
1199
  label: string;
package/dist/index.cjs CHANGED
@@ -1727,6 +1727,21 @@ function hasPnl(level) {
1727
1727
  function rectsOverlapY(ay, ah, by, bh) {
1728
1728
  return ay < by + bh && ay + ah > by;
1729
1729
  }
1730
+ function cascadeBracketAwayFromEntry(entry, preferredBoxY, boxH, direction, sameColumnObstacles) {
1731
+ const obstacles = [];
1732
+ if (entry) obstacles.push(entry);
1733
+ for (const o of sameColumnObstacles) obstacles.push(o);
1734
+ let boxY = preferredBoxY;
1735
+ let attempts = 0;
1736
+ const maxAttempts = obstacles.length + 2;
1737
+ while (attempts < maxAttempts) {
1738
+ const hit = obstacles.some((o) => rectsOverlapY(boxY, boxH, o.boxY, o.boxH));
1739
+ if (!hit) break;
1740
+ boxY += direction * (boxH + 2);
1741
+ attempts++;
1742
+ }
1743
+ return boxY;
1744
+ }
1730
1745
  function getLevelDisplayText(level, price, precision) {
1731
1746
  if (level.data?._isDraft) {
1732
1747
  const p = level;
@@ -1958,6 +1973,54 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
1958
1973
  selectedLabel,
1959
1974
  bracketBoxH
1960
1975
  );
1976
+ const bracketItems = [];
1977
+ const fallbackX = chartW * BRACKET_BASE_RATIO;
1978
+ for (const level of levels) {
1979
+ if (level.label !== hoveredLabel && level.label !== selectedLabel) continue;
1980
+ const bh = level;
1981
+ const mainDrag = draggingLabel === level.label;
1982
+ if (bh.stopLossPrice !== void 0) {
1983
+ const key = `${level.label}:stopLoss`;
1984
+ const ownDrag = draggingLabel === key;
1985
+ const price = ownDrag && dragNewPrice !== null ? dragNewPrice : mainDrag && dragNewSLPrice !== null ? dragNewSLPrice : bh.stopLossPrice;
1986
+ bracketItems.push({
1987
+ key,
1988
+ label: level.label,
1989
+ y: scale.yToPixel(price, priceRange, priceH),
1990
+ columnX: bracketLineXMap.get(key) ?? fallbackX
1991
+ });
1992
+ }
1993
+ if (bh.takeProfitPrice !== void 0) {
1994
+ const key = `${level.label}:takeProfit`;
1995
+ const ownDrag = draggingLabel === key;
1996
+ const price = ownDrag && dragNewPrice !== null ? dragNewPrice : mainDrag && dragNewTPPrice !== null ? dragNewTPPrice : bh.takeProfitPrice;
1997
+ bracketItems.push({
1998
+ key,
1999
+ label: level.label,
2000
+ y: scale.yToPixel(price, priceRange, priceH),
2001
+ columnX: bracketLineXMap.get(key) ?? fallbackX
2002
+ });
2003
+ }
2004
+ }
2005
+ bracketItems.sort((a, b) => a.y - b.y);
2006
+ const bracketBoxYMap = /* @__PURE__ */ new Map();
2007
+ const placedBrackets = [];
2008
+ for (const item of bracketItems) {
2009
+ const entry = layouts.get(item.label);
2010
+ const preferredBoxY = item.y - bracketBoxH / 2;
2011
+ const entryRect = entry ? { boxY: entry.boxY, boxH: entry.boxH } : null;
2012
+ const dir = entry ? Math.sign(item.y - entry.y) || 1 : 1;
2013
+ const sameColObstacles = placedBrackets.filter((p) => p.columnX === item.columnX);
2014
+ const newBoxY = cascadeBracketAwayFromEntry(
2015
+ entryRect,
2016
+ preferredBoxY,
2017
+ bracketBoxH,
2018
+ dir,
2019
+ sameColObstacles
2020
+ );
2021
+ bracketBoxYMap.set(item.key, newBoxY);
2022
+ placedBrackets.push({ columnX: item.columnX, boxY: newBoxY, boxH: bracketBoxH });
2023
+ }
1961
2024
  const activeLabel = hoveredLabel ?? selectedLabel;
1962
2025
  const bracketPnlHostLabel = draftOrderLabel ?? selectedLabel;
1963
2026
  const visibleMeasured = measured.filter((m) => !collapsedLabels.has(m.level.label));
@@ -2009,7 +2072,8 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
2009
2072
  draftQtyPillAreas,
2010
2073
  dragNewSLPrice,
2011
2074
  dragNewTPPrice,
2012
- buttonScale
2075
+ buttonScale,
2076
+ bracketBoxYMap
2013
2077
  );
2014
2078
  const ec = labelToExpandedCluster.get(level.label);
2015
2079
  if (ec && layout.boxY !== layout.y - boxH / 2) {
@@ -2022,7 +2086,6 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
2022
2086
  const bh = offLevel;
2023
2087
  const slDragKey = `${offLevel.label}:stopLoss`;
2024
2088
  const tpDragKey = `${offLevel.label}:takeProfit`;
2025
- const fallbackX = chartW * BRACKET_BASE_RATIO;
2026
2089
  const offMainDragActive = draggingLabel === offLevel.label;
2027
2090
  const offSlPrice = offMainDragActive && dragNewSLPrice !== null ? dragNewSLPrice : bh.stopLossPrice;
2028
2091
  const offTpPrice = offMainDragActive && dragNewTPPrice !== null ? dragNewTPPrice : bh.takeProfitPrice;
@@ -2046,7 +2109,8 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
2046
2109
  hideConfirmCancel,
2047
2110
  bh._slOrderLabel,
2048
2111
  void 0,
2049
- buttonScale
2112
+ buttonScale,
2113
+ bracketBoxYMap.get(slDragKey)
2050
2114
  );
2051
2115
  }
2052
2116
  if (offTpPrice !== void 0) {
@@ -2069,7 +2133,8 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
2069
2133
  hideConfirmCancel,
2070
2134
  bh._tpOrderLabel,
2071
2135
  void 0,
2072
- buttonScale
2136
+ buttonScale,
2137
+ bracketBoxYMap.get(tpDragKey)
2073
2138
  );
2074
2139
  }
2075
2140
  }
@@ -2215,7 +2280,7 @@ function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
2215
2280
  });
2216
2281
  ctx.restore();
2217
2282
  }
2218
- function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered, selected, scale, priceRange, priceH, draggingLabel, dragNewPrice, bracketLineXMap, hasPendingChanges, positionStyle, hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas, hideConfirmCancel = false, draftBracketPnl, showQuantityField = false, draftQtyPillAreas, dragNewSLPrice = null, dragNewTPPrice = null, buttonScale = 1) {
2283
+ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered, selected, scale, priceRange, priceH, draggingLabel, dragNewPrice, bracketLineXMap, hasPendingChanges, positionStyle, hitAreas, editAreas, hoverAreas, dragAreas, confirmAreas, addBracketAreas, hideConfirmCancel = false, draftBracketPnl, showQuantityField = false, draftQtyPillAreas, dragNewSLPrice = null, dragNewTPPrice = null, buttonScale = 1, bracketBoxYMap) {
2219
2284
  const { y, boxY, boxW, boxH } = layout;
2220
2285
  const color = levelColor(level, colors, theme);
2221
2286
  const active = hovered || selected;
@@ -2289,7 +2354,8 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
2289
2354
  hideConfirmCancel,
2290
2355
  bracketHost._slOrderLabel,
2291
2356
  draftBracketPnl?.sl,
2292
- buttonScale
2357
+ buttonScale,
2358
+ bracketBoxYMap?.get(slDragKey)
2293
2359
  );
2294
2360
  } else if (draggingLabel === slDragKey && dragNewPrice !== null) {
2295
2361
  const bLineX = chartW * BRACKET_BASE_RATIO;
@@ -2336,7 +2402,8 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
2336
2402
  hideConfirmCancel,
2337
2403
  bracketHost._tpOrderLabel,
2338
2404
  draftBracketPnl?.tp,
2339
- buttonScale
2405
+ buttonScale,
2406
+ bracketBoxYMap?.get(tpDragKey)
2340
2407
  );
2341
2408
  } else if (draggingLabel === tpDragKey && dragNewPrice !== null) {
2342
2409
  const bLineX = chartW * BRACKET_BASE_RATIO;
@@ -2670,7 +2737,7 @@ function drawAddBracketButton(ctx, level, bracketType, entryY, bracketLineX, cha
2670
2737
  h: bH
2671
2738
  });
2672
2739
  }
2673
- function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRange, priceH, bracketLineX, chartW, theme, precision, draggingLabel, dragNewPrice, hitAreas, dragAreas, hideConfirmCancel, bracketOrderLabel, pnlText, buttonScale = 1) {
2740
+ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRange, priceH, bracketLineX, chartW, theme, precision, draggingLabel, dragNewPrice, hitAreas, dragAreas, hideConfirmCancel, bracketOrderLabel, pnlText, buttonScale = 1, boxYOverride) {
2674
2741
  const closeR = CLOSE_R * Math.min(Math.max(buttonScale, 1), 3);
2675
2742
  const isDraggingThis = draggingLabel === `${level.label}:${bracketType}`;
2676
2743
  const price = isDraggingThis && dragNewPrice !== null ? dragNewPrice : bracketPrice;
@@ -2695,7 +2762,13 @@ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRang
2695
2762
  const bW = BRACKET_BOX_PAD_X + textW + pnlW + BRACKET_BOX_PAD_X + closeSlot;
2696
2763
  const bH = hideConfirmCancel ? BRACKET_BOX_H : Math.max(BRACKET_BOX_H, closeR * 2 + BRACKET_BOX_PAD_Y * 2);
2697
2764
  const bX = bracketLineX - bW - 2;
2698
- const bY = y - bH / 2;
2765
+ const naturalBoxY = y - bH / 2;
2766
+ const bY = boxYOverride ?? naturalBoxY;
2767
+ const boxCenterY = bY + bH / 2;
2768
+ const displaced = Math.abs(bY - naturalBoxY) >= 2;
2769
+ if (displaced) {
2770
+ drawBracketLeader(ctx, bX + bW / 2, boxCenterY, y, theme);
2771
+ }
2699
2772
  ctx.fillStyle = monoBoxBg(theme, false);
2700
2773
  ctx.strokeStyle = monoColor(theme);
2701
2774
  ctx.lineWidth = 0.8;
@@ -2705,15 +2778,15 @@ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRang
2705
2778
  ctx.fillStyle = monoColor(theme);
2706
2779
  ctx.textAlign = "left";
2707
2780
  ctx.textBaseline = "middle";
2708
- ctx.fillText(boxLabel, bX + BRACKET_BOX_PAD_X, y);
2781
+ ctx.fillText(boxLabel, bX + BRACKET_BOX_PAD_X, boxCenterY);
2709
2782
  if (pnlText) {
2710
2783
  const isLoss = pnlText.trim().startsWith("-");
2711
2784
  ctx.fillStyle = isLoss ? theme.candle.down : theme.candle.up;
2712
- ctx.fillText(pnlText, bX + BRACKET_BOX_PAD_X + textW + BRACKET_BOX_PAD_X, y);
2785
+ ctx.fillText(pnlText, bX + BRACKET_BOX_PAD_X + textW + BRACKET_BOX_PAD_X, boxCenterY);
2713
2786
  }
2714
2787
  if (!hideConfirmCancel) {
2715
2788
  const btnX = bX + bW - BRACKET_BOX_PAD_X / 2 - closeR;
2716
- const btnY = y;
2789
+ const btnY = boxCenterY;
2717
2790
  ctx.fillStyle = monoColor(theme);
2718
2791
  ctx.beginPath();
2719
2792
  ctx.arc(btnX, btnY, closeR, 0, Math.PI * 2);
@@ -2771,6 +2844,19 @@ function drawBracketLine(ctx, level, bracketType, bracketPrice, scale, priceRang
2771
2844
  isBracket: true
2772
2845
  });
2773
2846
  }
2847
+ function drawBracketLeader(ctx, leaderX, pillCenterY, realY, theme) {
2848
+ if (Math.abs(pillCenterY - realY) < 2) return;
2849
+ ctx.save();
2850
+ ctx.strokeStyle = monoColor(theme);
2851
+ ctx.globalAlpha = 0.35;
2852
+ ctx.lineWidth = 1;
2853
+ ctx.setLineDash([2, 3]);
2854
+ ctx.beginPath();
2855
+ ctx.moveTo(leaderX, pillCenterY);
2856
+ ctx.lineTo(leaderX, realY);
2857
+ ctx.stroke();
2858
+ ctx.restore();
2859
+ }
2774
2860
  function drawGripHandle(ctx, x, boxY, boxH, colors, hovered) {
2775
2861
  const dotColor = hovered ? colors.dragHandleHover : colors.dragHandle;
2776
2862
  const cols = 2;
@@ -17550,6 +17636,16 @@ var _ChartEngine = class _ChartEngine {
17550
17636
  this.expandedClusters = /* @__PURE__ */ new Set();
17551
17637
  this.hoveredTradeLabel = null;
17552
17638
  this.selectedTradeLabel = null;
17639
+ /**
17640
+ * Label of the level whose external edit form is currently open (i.e. the label for which
17641
+ * `tradeLevelEditOpen` was most recently emitted and no matching cancel/confirm has fired yet).
17642
+ * Distinct from `selectedTradeLabel`, which can also be set by a plain desktop hover-click
17643
+ * that just shows brackets without opening the form. Used so outside-click / cross-button
17644
+ * can issue a proper cancel (emit `tradeLevelEditCancelled`) even when no pending drag is
17645
+ * staged — e.g. the user opened the form on a position that has no SL/TP yet and clicks
17646
+ * elsewhere without adding anything.
17647
+ */
17648
+ this._editOpenLabel = null;
17553
17649
  /** Estimated PNL text for draft order SL/TP bracket lines, set by setDraftBracketPnl(). */
17554
17650
  this.draftBracketPnl = {};
17555
17651
  // TFC components
@@ -17984,6 +18080,30 @@ var _ChartEngine = class _ChartEngine {
17984
18080
  this._handleTradeLevelClose(closeHit);
17985
18081
  return;
17986
18082
  }
18083
+ if (!this.drawingManager.getActiveTool()) {
18084
+ const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
18085
+ if (editHit) {
18086
+ e.preventDefault();
18087
+ const level = this.levels.find((l) => l.label === editHit.label);
18088
+ if (level) {
18089
+ this.selectedTradeLabel = level.label;
18090
+ this.renderTradeLayer();
18091
+ const { slPrice: _slE, tpPrice: _tpE } = this._resolveBracketPrices(level);
18092
+ this._editOpenLabel = level.label;
18093
+ this.emitter.emit("tradeLevelEditOpen", {
18094
+ label: level.label,
18095
+ type: level.type,
18096
+ data: level.data,
18097
+ price: level.price,
18098
+ side: level.side,
18099
+ stopLossPrice: _slE,
18100
+ takeProfitPrice: _tpE,
18101
+ isFullscreen: this.isFullscreen
18102
+ });
18103
+ }
18104
+ return;
18105
+ }
18106
+ }
17987
18107
  if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
17988
18108
  const dragHit = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
17989
18109
  if (dragHit) {
@@ -18001,6 +18121,7 @@ var _ChartEngine = class _ChartEngine {
18001
18121
  }
18002
18122
  this.renderTradeLayer();
18003
18123
  const { slPrice: _sl1, tpPrice: _tp1 } = this._resolveBracketPrices(level);
18124
+ this._editOpenLabel = level.label;
18004
18125
  this.emitter.emit("tradeLevelEditOpen", {
18005
18126
  label: level.label,
18006
18127
  type: level.type,
@@ -18026,6 +18147,35 @@ var _ChartEngine = class _ChartEngine {
18026
18147
  return;
18027
18148
  }
18028
18149
  }
18150
+ if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
18151
+ const hoverHit = hitTestTradeLevelHover(x, y, this.tradeHoverAreas);
18152
+ if (hoverHit) {
18153
+ e.preventDefault();
18154
+ const level = this.levels.find((l) => l.label === hoverHit.label);
18155
+ if (level) {
18156
+ const hasPending = this.pendingLevelDrags.has(hoverHit.label);
18157
+ if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
18158
+ this.selectedTradeLabel = null;
18159
+ } else {
18160
+ this.selectedTradeLabel = hoverHit.label;
18161
+ }
18162
+ const { slPrice: _slH, tpPrice: _tpH } = this._resolveBracketPrices(level);
18163
+ this._editOpenLabel = level.label;
18164
+ this.renderTradeLayer();
18165
+ this.emitter.emit("tradeLevelEditOpen", {
18166
+ label: level.label,
18167
+ type: level.type,
18168
+ data: level.data,
18169
+ price: level.price,
18170
+ side: level.side,
18171
+ stopLossPrice: _slH,
18172
+ takeProfitPrice: _tpH,
18173
+ isFullscreen: this.isFullscreen
18174
+ });
18175
+ return;
18176
+ }
18177
+ }
18178
+ }
18029
18179
  this.drawingManager.selectDrawing(null);
18030
18180
  this.hideMobileDrawingFlyout();
18031
18181
  if (this.expandedClusters.size > 0) {
@@ -18302,27 +18452,6 @@ var _ChartEngine = class _ChartEngine {
18302
18452
  this._handleTradeLevelClose(closedLevel);
18303
18453
  return;
18304
18454
  }
18305
- if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
18306
- const dragHit = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
18307
- if (dragHit) {
18308
- const level = this.levels.find((l) => l.label === dragHit.label);
18309
- if (level) {
18310
- if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
18311
- this._startTradeDragFromHit(dragHit, level, y);
18312
- this.drawCanvas.style.cursor = "ns-resize";
18313
- return;
18314
- }
18315
- }
18316
- }
18317
- }
18318
- if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
18319
- const addHit = hitTestTradeLevelAddBracket(x, y, this.tradeAddBracketAreas);
18320
- if (addHit) {
18321
- this._startAddBracketFromHit(addHit, y);
18322
- this.drawCanvas.style.cursor = "ns-resize";
18323
- return;
18324
- }
18325
- }
18326
18455
  if (!this.drawingManager.getActiveTool()) {
18327
18456
  const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
18328
18457
  if (editHit) {
@@ -18331,6 +18460,7 @@ var _ChartEngine = class _ChartEngine {
18331
18460
  this.selectedTradeLabel = level.label;
18332
18461
  this.renderTradeLayer();
18333
18462
  const { slPrice: _sl2, tpPrice: _tp2 } = this._resolveBracketPrices(level);
18463
+ this._editOpenLabel = level.label;
18334
18464
  this.emitter.emit("tradeLevelEditOpen", {
18335
18465
  label: level.label,
18336
18466
  type: level.type,
@@ -18345,6 +18475,27 @@ var _ChartEngine = class _ChartEngine {
18345
18475
  return;
18346
18476
  }
18347
18477
  }
18478
+ if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
18479
+ const addHit = hitTestTradeLevelAddBracket(x, y, this.tradeAddBracketAreas);
18480
+ if (addHit) {
18481
+ this._startAddBracketFromHit(addHit, y);
18482
+ this.drawCanvas.style.cursor = "ns-resize";
18483
+ return;
18484
+ }
18485
+ }
18486
+ if (this.tradeDragHandler && !this.drawingManager.getActiveTool()) {
18487
+ const dragHit = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
18488
+ if (dragHit) {
18489
+ const level = this.levels.find((l) => l.label === dragHit.label);
18490
+ if (level) {
18491
+ if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
18492
+ this._startTradeDragFromHit(dragHit, level, y);
18493
+ this.drawCanvas.style.cursor = "ns-resize";
18494
+ return;
18495
+ }
18496
+ }
18497
+ }
18498
+ }
18348
18499
  const hoverHit = hitTestTradeLevelHover(x, y, this.tradeHoverAreas);
18349
18500
  if (hoverHit) {
18350
18501
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
@@ -18358,6 +18509,7 @@ var _ChartEngine = class _ChartEngine {
18358
18509
  const level = this.levels.find((l) => l.label === hoverHit.label);
18359
18510
  if (level) {
18360
18511
  const { slPrice: _sl3, tpPrice: _tp3 } = this._resolveBracketPrices(level);
18512
+ this._editOpenLabel = level.label;
18361
18513
  this.emitter.emit("tradeLevelEditOpen", {
18362
18514
  label: level.label,
18363
18515
  type: level.type,
@@ -18372,9 +18524,27 @@ var _ChartEngine = class _ChartEngine {
18372
18524
  }
18373
18525
  return;
18374
18526
  } else if (this.selectedTradeLabel !== null) {
18375
- if (!this.hideLevelConfirmCancel && !this.pendingLevelDrags.has(this.selectedTradeLabel)) {
18376
- this.selectedTradeLabel = null;
18377
- this.renderTradeLayer();
18527
+ if (!this.hideLevelConfirmCancel) {
18528
+ const lbl = this.selectedTradeLabel;
18529
+ const pending = this.pendingLevelDrags.get(lbl);
18530
+ const isEditFormOpen = this._editOpenLabel === lbl;
18531
+ if (!pending || pending.length === 0) {
18532
+ if (isEditFormOpen) {
18533
+ const lvl = this.levels.find((l) => l.label === lbl);
18534
+ this._editOpenLabel = null;
18535
+ if (lvl) {
18536
+ this.emitter.emit("tradeLevelEditCancelled", {
18537
+ label: lbl,
18538
+ type: lvl.type,
18539
+ isFullscreen: this.isFullscreen
18540
+ });
18541
+ }
18542
+ }
18543
+ this.selectedTradeLabel = null;
18544
+ this.renderTradeLayer();
18545
+ } else if (pending.every((c) => c.isNew)) {
18546
+ this.revertPendingChanges(lbl);
18547
+ }
18378
18548
  }
18379
18549
  }
18380
18550
  if (this.expandedClusters.size > 0) {
@@ -20086,9 +20256,11 @@ var _ChartEngine = class _ChartEngine {
20086
20256
  const idx = this.levels.findIndex((l) => l.label === label);
20087
20257
  if (idx === -1) return;
20088
20258
  const lvl = this.levels[idx];
20259
+ if (lvl.lots === qty) return;
20089
20260
  if (!this.originalQtyByLabel.has(label)) {
20090
20261
  this.originalQtyByLabel.set(label, lvl.lots);
20091
20262
  }
20263
+ const previousLots = this.originalQtyByLabel.get(label) ?? lvl.lots ?? qty;
20092
20264
  this.levels = [
20093
20265
  ...this.levels.slice(0, idx),
20094
20266
  { ...lvl, lots: qty },
@@ -20097,6 +20269,13 @@ var _ChartEngine = class _ChartEngine {
20097
20269
  if (label !== this.draftOrderLabel) {
20098
20270
  this.pendingQtyChanges.set(label, qty);
20099
20271
  }
20272
+ this.emitter.emit("tradeLevelQtyChange", {
20273
+ label,
20274
+ type: label === this.draftOrderLabel ? "draft" : lvl.type,
20275
+ newLots: qty,
20276
+ previousLots,
20277
+ isFullscreen: this.isFullscreen
20278
+ });
20100
20279
  this.renderTradeLayer();
20101
20280
  }
20102
20281
  openDraftQtyFlyout(pill) {
@@ -20305,7 +20484,17 @@ var _ChartEngine = class _ChartEngine {
20305
20484
  newLevels[i] = { ...ml, lots: pendingQty };
20306
20485
  }
20307
20486
  }
20308
- this.levels = [...this.levels.filter((l) => l.type !== type), ...newLevels];
20487
+ const preservedDraft = this.draftOrderLabel !== null ? this.levels.find(
20488
+ (l) => l.type === type && l.label === this.draftOrderLabel && !newLevels.some((n) => n.label === l.label)
20489
+ ) : void 0;
20490
+ this.levels = [
20491
+ ...this.levels.filter((l) => l.type !== type),
20492
+ ...newLevels,
20493
+ ...preservedDraft ? [preservedDraft] : []
20494
+ ];
20495
+ if (this.selectedTradeLabel !== null && !this.levels.some((l) => l.label === this.selectedTradeLabel)) {
20496
+ this.selectedTradeLabel = null;
20497
+ }
20309
20498
  this.recomputeRenderStyle();
20310
20499
  this.renderTradeLayer();
20311
20500
  return this;
@@ -21362,6 +21551,7 @@ var _ChartEngine = class _ChartEngine {
21362
21551
  if (!changes) return;
21363
21552
  const level = this.levels.find((l) => l.label === label);
21364
21553
  if (!level) return;
21554
+ if (this._editOpenLabel === label) this._editOpenLabel = null;
21365
21555
  if (label === this.draftOrderLabel) {
21366
21556
  const mainEntry = changes.find((c) => c.field === "main");
21367
21557
  const draft = level;
@@ -21455,6 +21645,15 @@ var _ChartEngine = class _ChartEngine {
21455
21645
  }
21456
21646
  this.originalQtyByLabel.delete(lbl);
21457
21647
  }
21648
+ const revertedLvl = this.levels.find((l) => l.label === lbl);
21649
+ if (revertedLvl) {
21650
+ if (this._editOpenLabel === lbl) this._editOpenLabel = null;
21651
+ this.emitter.emit("tradeLevelEditCancelled", {
21652
+ label: lbl,
21653
+ type: revertedLvl.type,
21654
+ isFullscreen: this.isFullscreen
21655
+ });
21656
+ }
21458
21657
  }
21459
21658
  this.selectedTradeLabel = null;
21460
21659
  this.draftBracketPnl = {};
@@ -22135,9 +22334,31 @@ var _ChartEngine = class _ChartEngine {
22135
22334
  return;
22136
22335
  }
22137
22336
  const isDraftOrder = closedLevel.label === this.draftOrderLabel;
22337
+ const pendingBeforeRevert = this.pendingLevelDrags.get(closedLevel.label);
22338
+ const isEditFormOpen = this._editOpenLabel === closedLevel.label;
22339
+ const onlyIsNewPending = !!pendingBeforeRevert && pendingBeforeRevert.length > 0 && pendingBeforeRevert.every((c) => c.isNew);
22340
+ const noPending = !pendingBeforeRevert || pendingBeforeRevert.length === 0;
22341
+ const isCancelUnsavedEdit = !isDraftOrder && isEditFormOpen && (onlyIsNewPending || noPending);
22138
22342
  this.revertPendingChanges(closedLevel.label);
22139
22343
  this.pendingLevelDrags.delete(closedLevel.label);
22140
22344
  if (isDraftOrder) return;
22345
+ if (isCancelUnsavedEdit) {
22346
+ if (noPending) {
22347
+ const lvl = this.levels.find((l) => l.label === closedLevel.label);
22348
+ this._editOpenLabel = null;
22349
+ this.selectedTradeLabel = null;
22350
+ this.renderTradeLayer();
22351
+ if (lvl) {
22352
+ this.emitter.emit("tradeLevelEditCancelled", {
22353
+ label: closedLevel.label,
22354
+ type: lvl.type,
22355
+ isFullscreen: this.isFullscreen
22356
+ });
22357
+ }
22358
+ }
22359
+ return;
22360
+ }
22361
+ if (this._editOpenLabel === closedLevel.label) this._editOpenLabel = null;
22141
22362
  const originalLevel = this.levels.find((l) => l.label === closedLevel.label);
22142
22363
  const isEntryOrder = originalLevel?.type === "position" && !!originalLevel.entryPriceEditable;
22143
22364
  const closeAction = closedLevel.type === "position" && !isEntryOrder ? "CLOSED" : "CANCELLED";