acttrader-charts 1.0.19 → 1.0.21

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/dist/index.cjs CHANGED
@@ -264,6 +264,8 @@ function resolveCssVarsInTheme(theme, container) {
264
264
  dropdownHoverBg: resolve(theme.indicatorOverlay.dropdownHoverBg)
265
265
  },
266
266
  tradeLevels: {
267
+ buySide: resolve(theme.tradeLevels.buySide),
268
+ sellSide: resolve(theme.tradeLevels.sellSide),
267
269
  tradeLine: resolve(theme.tradeLevels.tradeLine),
268
270
  positionLine: resolve(theme.tradeLevels.positionLine),
269
271
  pendingBuyLine: resolve(theme.tradeLevels.pendingBuyLine),
@@ -340,6 +342,8 @@ var DARK_THEME = {
340
342
  dropdownHoverBg: "#1c2a35"
341
343
  },
342
344
  tradeLevels: {
345
+ buySide: "#26a69a",
346
+ sellSide: "#ef5350",
343
347
  tradeLine: "#5caf79",
344
348
  positionLine: "#7b61ff",
345
349
  pendingBuyLine: "rgba(92,175,121,0.75)",
@@ -412,6 +416,8 @@ var LIGHT_THEME = {
412
416
  dropdownHoverBg: "#f0f0f0"
413
417
  },
414
418
  tradeLevels: {
419
+ buySide: "#26a69a",
420
+ sellSide: "#ef5350",
415
421
  tradeLine: "#5caf79",
416
422
  positionLine: "#6d28d9",
417
423
  pendingBuyLine: "rgba(92,175,121,0.75)",
@@ -697,6 +703,7 @@ var DEFAULT_LABELS = {
697
703
  limit: "Limit",
698
704
  stop: "Stop",
699
705
  market: "Market",
706
+ at: "at",
700
707
  qty: "Qty",
701
708
  placeOrderTitle: "Place order at this price"
702
709
  },
@@ -1672,16 +1679,18 @@ function computeFanOutLayout(cluster, boxH, gapPx = 4) {
1672
1679
  return result;
1673
1680
  }
1674
1681
  function buildClusterBadgeLayout(cluster, chartW, ctx) {
1675
- const n = cluster.members.length;
1676
- const types = new Set(cluster.members.map((m) => m.level.type));
1677
- let noun;
1678
- if (types.size === 1) {
1679
- const t = types.values().next().value;
1680
- noun = t === "position" ? "Position" : t === "pending" ? "Order" : "Trade";
1681
- } else {
1682
- noun = "Level";
1683
- }
1684
- const text = `${n} ${noun}${n > 1 ? "s" : ""}`;
1682
+ const posCount = cluster.members.filter(
1683
+ (m) => m.level.type === "position" && !m.level.entryPriceEditable
1684
+ ).length;
1685
+ const orderCount = cluster.members.filter(
1686
+ (m) => m.level.type === "pending" || m.level.entryPriceEditable === true
1687
+ ).length;
1688
+ const tradeCount = cluster.members.filter((m) => m.level.type === "trade").length;
1689
+ const parts = [];
1690
+ if (posCount > 0) parts.push(`${posCount} Position${posCount > 1 ? "s" : ""}`);
1691
+ if (orderCount > 0) parts.push(`${orderCount} Order${orderCount > 1 ? "s" : ""}`);
1692
+ if (tradeCount > 0) parts.push(`${tradeCount} Trade${tradeCount > 1 ? "s" : ""}`);
1693
+ const text = parts.join(" / ");
1685
1694
  const PAD_X = 12;
1686
1695
  const textW = ctx.measureText(text).width;
1687
1696
  const EXPAND_ICON_W = 16;
@@ -2089,6 +2098,22 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
2089
2098
  const offMainDragActive = draggingLabel === offLevel.label;
2090
2099
  const offSlPrice = offMainDragActive && dragNewSLPrice !== null ? dragNewSLPrice : bh.stopLossPrice;
2091
2100
  const offTpPrice = offMainDragActive && dragNewTPPrice !== null ? dragNewTPPrice : bh.takeProfitPrice;
2101
+ if (offSlPrice !== void 0 && offTpPrice !== void 0) {
2102
+ const slY = scale.yToPixel(offSlPrice, priceRange, priceH);
2103
+ const tpY = scale.yToPixel(offTpPrice, priceRange, priceH);
2104
+ const bracketBH = Math.max(BRACKET_BOX_H, CLOSE_R * 2 + BRACKET_BOX_PAD_Y * 2);
2105
+ ctx.save();
2106
+ ctx.font = FONT_BRACKET;
2107
+ const closeSlot = CLOSE_R * 2 + BRACKET_BOX_PAD_X / 2;
2108
+ const slBW = BRACKET_BOX_PAD_X + ctx.measureText(`SL ${offSlPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2109
+ const tpBW = BRACKET_BOX_PAD_X + ctx.measureText(`TP ${offTpPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2110
+ ctx.restore();
2111
+ const connX = ((bracketLineXMap.get(slDragKey) ?? fallbackX) - slBW / 2 - 2 + ((bracketLineXMap.get(tpDragKey) ?? fallbackX) - tpBW / 2 - 2)) / 2;
2112
+ const slPillY = bracketBoxYMap.get(slDragKey) ?? slY - bracketBH / 2;
2113
+ const tpPillY = bracketBoxYMap.get(tpDragKey) ?? tpY - bracketBH / 2;
2114
+ const [upperY, lowerY] = tpPillY < slPillY ? [tpPillY, slPillY] : [slPillY, tpPillY];
2115
+ drawSLTPConnector(ctx, connX, upperY + bracketBH, lowerY, theme);
2116
+ }
2092
2117
  if (offSlPrice !== void 0) {
2093
2118
  drawBracketLine(
2094
2119
  ctx,
@@ -2222,6 +2247,20 @@ function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
2222
2247
  ctx.stroke();
2223
2248
  ctx.restore();
2224
2249
  }
2250
+ function drawSLTPConnector(ctx, x, yTop, yBottom, theme) {
2251
+ if (yBottom - yTop < 2) return;
2252
+ ctx.save();
2253
+ ctx.strokeStyle = monoColor(theme);
2254
+ ctx.globalAlpha = 0.5;
2255
+ ctx.lineWidth = 1;
2256
+ ctx.setLineDash([3, 4]);
2257
+ ctx.beginPath();
2258
+ ctx.moveTo(x, yTop);
2259
+ ctx.lineTo(x, yBottom);
2260
+ ctx.stroke();
2261
+ ctx.setLineDash([]);
2262
+ ctx.restore();
2263
+ }
2225
2264
  function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
2226
2265
  const pl = level;
2227
2266
  const rows = [];
@@ -2333,6 +2372,24 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
2333
2372
  const mainDragActive = draggingLabel === level.label;
2334
2373
  const slRenderPrice = mainDragActive && dragNewSLPrice !== null ? dragNewSLPrice : bracketHost.stopLossPrice;
2335
2374
  const tpRenderPrice = mainDragActive && dragNewTPPrice !== null ? dragNewTPPrice : bracketHost.takeProfitPrice;
2375
+ if (selected && slRenderPrice !== void 0 && tpRenderPrice !== void 0) {
2376
+ const slY = scale.yToPixel(slRenderPrice, priceRange, priceH);
2377
+ const tpY = scale.yToPixel(tpRenderPrice, priceRange, priceH);
2378
+ const bracketBH = hideConfirmCancel ? BRACKET_BOX_H : Math.max(BRACKET_BOX_H, closeR * 2 + BRACKET_BOX_PAD_Y * 2);
2379
+ ctx.save();
2380
+ ctx.font = FONT_BRACKET;
2381
+ const closeSlot = hideConfirmCancel ? 0 : closeR * 2 + BRACKET_BOX_PAD_X / 2;
2382
+ const slBW = BRACKET_BOX_PAD_X + ctx.measureText(`SL ${slRenderPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2383
+ const tpBW = BRACKET_BOX_PAD_X + ctx.measureText(`TP ${tpRenderPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2384
+ ctx.restore();
2385
+ const slBLineX = bracketLineXMap.get(slDragKey) ?? chartW * BRACKET_BASE_RATIO;
2386
+ const tpBLineX = bracketLineXMap.get(tpDragKey) ?? chartW * BRACKET_BASE_RATIO;
2387
+ const connX = (slBLineX - slBW / 2 - 2 + (tpBLineX - tpBW / 2 - 2)) / 2;
2388
+ const slPillY = bracketBoxYMap?.get(slDragKey) ?? slY - bracketBH / 2;
2389
+ const tpPillY = bracketBoxYMap?.get(tpDragKey) ?? tpY - bracketBH / 2;
2390
+ const [upperY, lowerY] = tpPillY < slPillY ? [tpPillY, slPillY] : [slPillY, tpPillY];
2391
+ drawSLTPConnector(ctx, connX, upperY + bracketBH, lowerY, theme);
2392
+ }
2336
2393
  if (slRenderPrice !== void 0) {
2337
2394
  const bLineX = bracketLineXMap.get(slDragKey) ?? chartW * BRACKET_BASE_RATIO;
2338
2395
  drawBracketLine(
@@ -11331,6 +11388,14 @@ var DrawingManager = class {
11331
11388
  this.onDrawingsChange = null;
11332
11389
  /** Called when a text-type drawing is first completed — ChartEngine shows the text input. */
11333
11390
  this.onStartTextEdit = null;
11391
+ /**
11392
+ * Called after drag/resize mutates a drawing's points so ChartEngine can
11393
+ * refresh each point's `timestamp` from the current dataStore. Required
11394
+ * because DrawingManager has no access to the data layer, and drag
11395
+ * reconstructs points from raw (barIndex, price) deltas that strip the
11396
+ * original point's timestamp.
11397
+ */
11398
+ this.onPointsMutated = null;
11334
11399
  /** Set to true by ChartEngine while the text input is focused — prevents Delete/Backspace from deleting the drawing. */
11335
11400
  this.editingText = false;
11336
11401
  // Drag state (move whole drawing)
@@ -11425,14 +11490,18 @@ var DrawingManager = class {
11425
11490
  const dBar = point.barIndex - this.dragStartMouse.barIndex;
11426
11491
  const dPrice = point.price - this.dragStartMouse.price;
11427
11492
  for (let i = 0; i < this.dragDrawing.points.length; i++) {
11493
+ const start = this.dragStartPoints[i];
11494
+ if (!start) continue;
11428
11495
  this.dragDrawing.points[i] = {
11429
- barIndex: this.dragStartPoints[i].barIndex + dBar,
11430
- price: this.dragStartPoints[i].price + dPrice
11496
+ barIndex: start.barIndex + dBar,
11497
+ price: start.price + dPrice
11431
11498
  };
11432
11499
  }
11500
+ this.onPointsMutated?.(this.dragDrawing);
11433
11501
  }
11434
11502
  if (this.resizeDrawing && this.resizePointIndex >= 0) {
11435
11503
  this.resizeDrawing.points[this.resizePointIndex] = { ...point };
11504
+ this.onPointsMutated?.(this.resizeDrawing);
11436
11505
  }
11437
11506
  }
11438
11507
  /**
@@ -13189,8 +13258,8 @@ var TradeButton = class {
13189
13258
  this.el.style.opacity = "1";
13190
13259
  Object.assign(this.el.style, {
13191
13260
  background: theme.tooltip.background,
13192
- color: "#26a69a",
13193
- border: "1px solid #26a69a"
13261
+ color: theme.tradeLevels.buySide,
13262
+ border: `1px solid ${theme.tradeLevels.buySide}`
13194
13263
  });
13195
13264
  });
13196
13265
  this.el.addEventListener("mouseleave", (e) => {
@@ -13253,11 +13322,12 @@ var TradeButton = class {
13253
13322
 
13254
13323
  // src/ui/TradePopover.ts
13255
13324
  var TradePopover = class {
13256
- constructor(theme, lots, onDraftCreate, labels = DEFAULT_LABELS.trade, onVisibilityChange) {
13325
+ constructor(theme, lots, symbol, onDraftCreate, labels = DEFAULT_LABELS.trade, onVisibilityChange) {
13257
13326
  this._price = 0;
13258
13327
  this._pricePrecision = 2;
13259
13328
  this._buyOrderType = "limit";
13260
13329
  this._sellOrderType = "limit";
13330
+ this._compact = false;
13261
13331
  this.onDocMouseDown = (e) => {
13262
13332
  if (this.isVisible && !this.el.contains(e.target)) {
13263
13333
  this.hide();
@@ -13265,6 +13335,7 @@ var TradePopover = class {
13265
13335
  };
13266
13336
  this.theme = theme;
13267
13337
  this.lots = lots;
13338
+ this.symbol = symbol;
13268
13339
  this.onDraftCreate = onDraftCreate;
13269
13340
  this.labels = labels;
13270
13341
  this.onVisibilityChange = onVisibilityChange;
@@ -13273,7 +13344,6 @@ var TradePopover = class {
13273
13344
  position: "absolute",
13274
13345
  display: "none",
13275
13346
  flexDirection: "column",
13276
- gap: "1px",
13277
13347
  background: theme.tooltip.background,
13278
13348
  border: `1px solid ${theme.axisBorder}`,
13279
13349
  borderRadius: "5px",
@@ -13282,7 +13352,7 @@ var TradePopover = class {
13282
13352
  boxShadow: "0 4px 16px rgba(0,0,0,0.25)",
13283
13353
  fontFamily: "'Inter', system-ui, sans-serif",
13284
13354
  fontSize: "12px",
13285
- minWidth: "140px",
13355
+ width: "max-content",
13286
13356
  pointerEvents: "auto"
13287
13357
  });
13288
13358
  document.addEventListener("mousedown", this.onDocMouseDown);
@@ -13296,10 +13366,8 @@ var TradePopover = class {
13296
13366
  this._sellOrderType = sellOrderType;
13297
13367
  this.rebuild();
13298
13368
  this.el.style.display = "flex";
13299
- const popW = 145;
13300
13369
  this.el.style.right = `${buttonRight}px`;
13301
13370
  this.el.style.top = `${buttonY - 16}px`;
13302
- this.el.style.width = `${popW}px`;
13303
13371
  if (!wasVisible) this.onVisibilityChange?.();
13304
13372
  }
13305
13373
  hide() {
@@ -13313,6 +13381,12 @@ var TradePopover = class {
13313
13381
  setLots(lots) {
13314
13382
  this.lots = lots;
13315
13383
  }
13384
+ setSymbol(symbol) {
13385
+ this.symbol = symbol;
13386
+ }
13387
+ setCompact(compact) {
13388
+ this._compact = compact;
13389
+ }
13316
13390
  setTheme(theme) {
13317
13391
  this.theme = theme;
13318
13392
  Object.assign(this.el.style, {
@@ -13326,31 +13400,46 @@ var TradePopover = class {
13326
13400
  // ── Private ───────────────────────────────────────────────────────────────
13327
13401
  rebuild() {
13328
13402
  this.el.innerHTML = "";
13403
+ const divider = document.createElement("div");
13404
+ Object.assign(divider.style, {
13405
+ height: "1px",
13406
+ background: this.theme.axisBorder,
13407
+ margin: "0 8px"
13408
+ });
13329
13409
  this.el.append(
13330
- this.makeRow(this.labels.buy, "#26a69a", "buy", this._buyOrderType),
13331
- this.makeRow(this.labels.sell, "#ef5350", "sell", this._sellOrderType)
13410
+ this.makeRow(this.theme.tradeLevels.sellSide, "sell", this._sellOrderType),
13411
+ divider,
13412
+ this.makeRow(this.theme.tradeLevels.buySide, "buy", this._buyOrderType)
13332
13413
  );
13333
13414
  }
13334
- makeRow(label, color, side, orderType) {
13415
+ makeRow(color, side, orderType) {
13335
13416
  const btn = document.createElement("button");
13336
- const priceStr = this._price.toFixed(this._pricePrecision);
13337
- btn.textContent = `${label} ${orderType === "limit" ? this.labels.limit : this.labels.stop} ${priceStr}`;
13338
13417
  Object.assign(btn.style, {
13339
- display: "block",
13418
+ display: "flex",
13419
+ alignItems: "center",
13420
+ gap: "8px",
13340
13421
  width: "100%",
13341
13422
  background: "transparent",
13342
- color,
13343
13423
  border: "none",
13344
13424
  borderRadius: "3px",
13345
- padding: "7px 10px",
13346
- textAlign: "left",
13425
+ padding: this._compact ? "5px 8px" : "8px 10px",
13347
13426
  cursor: "pointer",
13348
- fontSize: "12px",
13349
13427
  fontFamily: "'Inter', system-ui, sans-serif",
13350
- fontWeight: "600"
13428
+ fontSize: this._compact ? "11px" : "12px"
13351
13429
  });
13430
+ btn.appendChild(this.makeChevron(color, side));
13431
+ const sideLabel = side === "sell" ? this.labels.sell : this.labels.buy;
13432
+ const orderTypeLabel = orderType === "limit" ? this.labels.limit : this.labels.stop;
13433
+ const priceStr = this._price.toFixed(this._pricePrecision);
13434
+ const colored = document.createElement("span");
13435
+ colored.textContent = `${sideLabel} ${orderTypeLabel}`;
13436
+ Object.assign(colored.style, { color, fontWeight: "600" });
13437
+ const plain = document.createElement("span");
13438
+ plain.textContent = ` ${this.lots} ${this.symbol} ${this.labels.at} ${priceStr}`;
13439
+ Object.assign(plain.style, { color: "#ffffff", fontWeight: "400" });
13440
+ btn.append(colored, plain);
13352
13441
  btn.addEventListener("mouseenter", () => {
13353
- btn.style.background = color + "22";
13442
+ btn.style.background = "rgba(255,255,255,0.06)";
13354
13443
  });
13355
13444
  btn.addEventListener("mouseleave", () => {
13356
13445
  btn.style.background = "transparent";
@@ -13362,6 +13451,23 @@ var TradePopover = class {
13362
13451
  });
13363
13452
  return btn;
13364
13453
  }
13454
+ makeChevron(color, side) {
13455
+ const ns = "http://www.w3.org/2000/svg";
13456
+ const svg = document.createElementNS(ns, "svg");
13457
+ svg.setAttribute("width", "10");
13458
+ svg.setAttribute("height", "8");
13459
+ svg.setAttribute("viewBox", "0 0 10 8");
13460
+ svg.style.flexShrink = "0";
13461
+ const path = document.createElementNS(ns, "path");
13462
+ path.setAttribute("d", side === "sell" ? "M1 1 L5 7 L9 1" : "M1 7 L5 1 L9 7");
13463
+ path.setAttribute("stroke", color);
13464
+ path.setAttribute("fill", "none");
13465
+ path.setAttribute("stroke-width", "1.5");
13466
+ path.setAttribute("stroke-linecap", "round");
13467
+ path.setAttribute("stroke-linejoin", "round");
13468
+ svg.appendChild(path);
13469
+ return svg;
13470
+ }
13365
13471
  };
13366
13472
 
13367
13473
  // src/ui/DraftQtyFlyout.ts
@@ -17572,6 +17678,13 @@ var _ChartEngine = class _ChartEngine {
17572
17678
  /** Pending resize — committed only after mouse moves past DRAW_GESTURE_THRESHOLD. */
17573
17679
  this._pendingDrawResize = null;
17574
17680
  // px
17681
+ /**
17682
+ * Pending trade-level drag — committed only after mouse moves past TRADE_DRAG_PIXEL_THRESHOLD.
17683
+ * A mouseup before the threshold means it was a click: route to _openTradeLevelEditForm instead
17684
+ * of finishing a zero-delta drag (which would otherwise stage a spurious pending change).
17685
+ */
17686
+ this._pendingTradeDrag = null;
17687
+ // px
17575
17688
  // Pane resize
17576
17689
  this.paneHeightOverrides = /* @__PURE__ */ new Map();
17577
17690
  this.separatorDrag = null;
@@ -17587,6 +17700,21 @@ var _ChartEngine = class _ChartEngine {
17587
17700
  // lerp target; null = snap on next render
17588
17701
  this._lockedAutoFitBase = null;
17589
17702
  // non-null = user has manually adjusted Y-axis
17703
+ /**
17704
+ * When setState() restores a persisted viewport but data isn't loaded yet,
17705
+ * we stash the desired time window here and apply it from loadData() once
17706
+ * bars are available. Cleared after one application.
17707
+ */
17708
+ this._pendingViewportRestore = null;
17709
+ /**
17710
+ * Debounces viewport/priceScale stateChange emissions during continuous
17711
+ * gestures (pan, wheel-zoom, price-scale drag). Pan/zoom don't emit
17712
+ * stateChange directly because doing so per-frame would be wasteful; this
17713
+ * timer coalesces bursts so the persisted viewport catches up a few ms
17714
+ * after the user stops interacting. Paired with the consumer's own
17715
+ * debounce (e.g. CRM's 800ms save) for minimal traffic.
17716
+ */
17717
+ this._viewportPersistTimer = null;
17590
17718
  this.axisDrag = null;
17591
17719
  this.subPaneScaleFactors = /* @__PURE__ */ new Map();
17592
17720
  this._mobileCrosshairActive = false;
@@ -17747,14 +17875,14 @@ var _ChartEngine = class _ChartEngine {
17747
17875
  const dx = x - this._pendingDrawDrag.startX;
17748
17876
  const dy = y - this._pendingDrawDrag.startY;
17749
17877
  if (Math.sqrt(dx * dx + dy * dy) > _ChartEngine.DRAW_GESTURE_THRESHOLD) {
17750
- this.drawingManager.startDrag(this._pendingDrawDrag.drawing, {
17751
- barIndex: rawBarIndex,
17752
- price
17753
- });
17878
+ this.drawingManager.startDrag(
17879
+ this._pendingDrawDrag.drawing,
17880
+ this.makeDrawingPoint(rawBarIndex, price)
17881
+ );
17754
17882
  this._pendingDrawDrag = null;
17755
17883
  }
17756
17884
  }
17757
- this.drawingManager.onMouseMove({ barIndex: rawBarIndex, price });
17885
+ this.drawingManager.onMouseMove(this.makeDrawingPoint(rawBarIndex, price));
17758
17886
  if (this.axisDrag) {
17759
17887
  if (this.axisDrag.axis === "price") {
17760
17888
  const delta = y - this.axisDrag.startPos;
@@ -17796,6 +17924,18 @@ var _ChartEngine = class _ChartEngine {
17796
17924
  this.scheduleRender();
17797
17925
  return;
17798
17926
  }
17927
+ if (this._pendingTradeDrag) {
17928
+ const dx = x - this._pendingTradeDrag.startX;
17929
+ const dy = y - this._pendingTradeDrag.startY;
17930
+ if (Math.sqrt(dx * dx + dy * dy) > _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD) {
17931
+ const { dragHit, level, hitY } = this._pendingTradeDrag;
17932
+ this._pendingTradeDrag = null;
17933
+ this._startTradeDragFromHit(dragHit, level, hitY);
17934
+ this.drawCanvas.style.cursor = "ns-resize";
17935
+ } else {
17936
+ return;
17937
+ }
17938
+ }
17799
17939
  if (this.tradeDragHandler?.active) {
17800
17940
  const moveResult = this.tradeDragHandler.onMouseMove(
17801
17941
  y,
@@ -17836,6 +17976,10 @@ var _ChartEngine = class _ChartEngine {
17836
17976
  const dragHover = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
17837
17977
  if (dragHover) {
17838
17978
  this.drawCanvas.style.cursor = "ns-resize";
17979
+ if (dragHover.label !== this.hoveredTradeLabel) {
17980
+ this.hoveredTradeLabel = dragHover.label;
17981
+ this.renderTradeLayer();
17982
+ }
17839
17983
  this.scheduleRender();
17840
17984
  return;
17841
17985
  }
@@ -17905,6 +18049,16 @@ var _ChartEngine = class _ChartEngine {
17905
18049
  this.scheduleRender();
17906
18050
  };
17907
18051
  this.onMouseUp = () => {
18052
+ if (this._pendingTradeDrag) {
18053
+ const { level } = this._pendingTradeDrag;
18054
+ this._pendingTradeDrag = null;
18055
+ if (this._editOpenLabel === level.label && !this.pendingLevelDrags.has(level.label)) {
18056
+ this._cancelEditAndDeselect(level.label);
18057
+ return;
18058
+ }
18059
+ this._openTradeLevelEditForm(level);
18060
+ return;
18061
+ }
17908
18062
  if (this.tradeDragHandler?.active) {
17909
18063
  const result = this.tradeDragHandler.endDrag();
17910
18064
  if (result) {
@@ -17921,9 +18075,11 @@ var _ChartEngine = class _ChartEngine {
17921
18075
  return;
17922
18076
  }
17923
18077
  if (this.axisDrag) {
18078
+ const wasPriceAxis = this.axisDrag.axis === "price" || this.axisDrag.axis === "time";
17924
18079
  this.axisDrag = null;
17925
18080
  this.drawCanvas.style.cursor = "default";
17926
18081
  this.scheduleRender();
18082
+ if (wasPriceAxis) this.scheduleViewportPersist();
17927
18083
  }
17928
18084
  if (this.separatorDrag) {
17929
18085
  this.separatorDrag = null;
@@ -18085,22 +18241,7 @@ var _ChartEngine = class _ChartEngine {
18085
18241
  if (editHit) {
18086
18242
  e.preventDefault();
18087
18243
  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
- }
18244
+ if (level) this._openTradeLevelEditForm(level);
18104
18245
  return;
18105
18246
  }
18106
18247
  }
@@ -18115,23 +18256,10 @@ var _ChartEngine = class _ChartEngine {
18115
18256
  if (this.hideLevelConfirmCancel && !isMobileDraggable) {
18116
18257
  const hasPending = this.pendingLevelDrags.has(level.label);
18117
18258
  if (this.selectedTradeLabel === level.label && !hasPending) {
18118
- this.selectedTradeLabel = null;
18119
- } else {
18120
- this.selectedTradeLabel = level.label;
18259
+ this._cancelEditAndDeselect(level.label);
18260
+ return;
18121
18261
  }
18122
- this.renderTradeLayer();
18123
- const { slPrice: _sl1, tpPrice: _tp1 } = this._resolveBracketPrices(level);
18124
- this._editOpenLabel = level.label;
18125
- this.emitter.emit("tradeLevelEditOpen", {
18126
- label: level.label,
18127
- type: level.type,
18128
- data: level.data,
18129
- price: level.price,
18130
- side: level.side,
18131
- stopLossPrice: _sl1,
18132
- takeProfitPrice: _tp1,
18133
- isFullscreen: this.isFullscreen
18134
- });
18262
+ this._openTradeLevelEditForm(level);
18135
18263
  return;
18136
18264
  }
18137
18265
  this._startTradeDragFromHit(dragHit, level, y);
@@ -18155,23 +18283,10 @@ var _ChartEngine = class _ChartEngine {
18155
18283
  if (level) {
18156
18284
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
18157
18285
  if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
18158
- this.selectedTradeLabel = null;
18159
- } else {
18160
- this.selectedTradeLabel = hoverHit.label;
18286
+ this._cancelEditAndDeselect(hoverHit.label);
18287
+ return;
18161
18288
  }
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
- });
18289
+ this._openTradeLevelEditForm(level);
18175
18290
  return;
18176
18291
  }
18177
18292
  }
@@ -18370,8 +18485,10 @@ var _ChartEngine = class _ChartEngine {
18370
18485
  return;
18371
18486
  }
18372
18487
  if (this.axisDrag) {
18488
+ const wasPriceAxis = this.axisDrag.axis === "price" || this.axisDrag.axis === "time";
18373
18489
  this.axisDrag = null;
18374
18490
  this.scheduleRender();
18491
+ if (wasPriceAxis) this.scheduleViewportPersist();
18375
18492
  return;
18376
18493
  }
18377
18494
  const wasAnchorTouch = this._drawTouchAnchorActive;
@@ -18456,22 +18573,7 @@ var _ChartEngine = class _ChartEngine {
18456
18573
  const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
18457
18574
  if (editHit) {
18458
18575
  const level = this.levels.find((l) => l.label === editHit.label);
18459
- if (level) {
18460
- this.selectedTradeLabel = level.label;
18461
- this.renderTradeLayer();
18462
- const { slPrice: _sl2, tpPrice: _tp2 } = this._resolveBracketPrices(level);
18463
- this._editOpenLabel = level.label;
18464
- this.emitter.emit("tradeLevelEditOpen", {
18465
- label: level.label,
18466
- type: level.type,
18467
- data: level.data,
18468
- price: level.price,
18469
- side: level.side,
18470
- stopLossPrice: _sl2,
18471
- takeProfitPrice: _tp2,
18472
- isFullscreen: this.isFullscreen
18473
- });
18474
- }
18576
+ if (level) this._openTradeLevelEditForm(level);
18475
18577
  return;
18476
18578
  }
18477
18579
  }
@@ -18489,8 +18591,7 @@ var _ChartEngine = class _ChartEngine {
18489
18591
  const level = this.levels.find((l) => l.label === dragHit.label);
18490
18592
  if (level) {
18491
18593
  if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
18492
- this._startTradeDragFromHit(dragHit, level, y);
18493
- this.drawCanvas.style.cursor = "ns-resize";
18594
+ this._pendingTradeDrag = { dragHit, level, hitY: y, startX: x, startY: y };
18494
18595
  return;
18495
18596
  }
18496
18597
  }
@@ -18500,49 +18601,24 @@ var _ChartEngine = class _ChartEngine {
18500
18601
  if (hoverHit) {
18501
18602
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
18502
18603
  if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
18503
- this.selectedTradeLabel = null;
18604
+ this._cancelEditAndDeselect(hoverHit.label);
18605
+ return;
18606
+ }
18607
+ const level = this.levels.find((l) => l.label === hoverHit.label);
18608
+ if (level) {
18609
+ this._openTradeLevelEditForm(level);
18504
18610
  } else {
18505
18611
  this.selectedTradeLabel = hoverHit.label;
18506
- }
18507
- this.renderTradeLayer();
18508
- if (this.hideLevelConfirmCancel) {
18509
- const level = this.levels.find((l) => l.label === hoverHit.label);
18510
- if (level) {
18511
- const { slPrice: _sl3, tpPrice: _tp3 } = this._resolveBracketPrices(level);
18512
- this._editOpenLabel = level.label;
18513
- this.emitter.emit("tradeLevelEditOpen", {
18514
- label: level.label,
18515
- type: level.type,
18516
- data: level.data,
18517
- price: level.price,
18518
- side: level.side,
18519
- stopLossPrice: _sl3,
18520
- takeProfitPrice: _tp3,
18521
- isFullscreen: this.isFullscreen
18522
- });
18523
- }
18612
+ this.renderTradeLayer();
18524
18613
  }
18525
18614
  return;
18526
18615
  } else if (this.selectedTradeLabel !== null) {
18527
18616
  if (!this.hideLevelConfirmCancel) {
18528
18617
  const lbl = this.selectedTradeLabel;
18529
18618
  const pending = this.pendingLevelDrags.get(lbl);
18530
- const isEditFormOpen = this._editOpenLabel === lbl;
18531
18619
  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)) {
18620
+ this._cancelEditAndDeselect(lbl);
18621
+ } else {
18546
18622
  this.revertPendingChanges(lbl);
18547
18623
  }
18548
18624
  }
@@ -18597,10 +18673,9 @@ var _ChartEngine = class _ChartEngine {
18597
18673
  this.scheduleRender();
18598
18674
  return;
18599
18675
  }
18600
- const consumed = this.drawingManager.onMouseDown({
18601
- barIndex: rawBarIndex,
18602
- price
18603
- });
18676
+ const consumed = this.drawingManager.onMouseDown(
18677
+ this.makeDrawingPoint(rawBarIndex, price)
18678
+ );
18604
18679
  if (!consumed) {
18605
18680
  const handleHit = this.drawingManager.getHandleAtPoint(
18606
18681
  x,
@@ -19195,6 +19270,7 @@ var _ChartEngine = class _ChartEngine {
19195
19270
  this.tradePopover = new TradePopover(
19196
19271
  this.theme,
19197
19272
  0.01,
19273
+ this.symbol,
19198
19274
  (side, price, orderType) => {
19199
19275
  this.createDraftOrder(side, price, orderType);
19200
19276
  },
@@ -19298,6 +19374,12 @@ var _ChartEngine = class _ChartEngine {
19298
19374
  this.drawCanvas.style.cursor = "default";
19299
19375
  });
19300
19376
  this.drawingManager.onDrawingsChange = () => this.emitStateChange();
19377
+ this.drawingManager.onPointsMutated = (drawing) => {
19378
+ for (const pt of drawing.points) {
19379
+ const t = this.barIndexToTimestamp(pt.barIndex);
19380
+ if (t != null) pt.timestamp = t;
19381
+ }
19382
+ };
19301
19383
  this.drawingManager.onStartTextEdit = (drawing) => this.showTextInput(drawing);
19302
19384
  this.panHandler = new PanHandler(
19303
19385
  this.drawCanvas,
@@ -19307,16 +19389,19 @@ var _ChartEngine = class _ChartEngine {
19307
19389
  this.clearDuration();
19308
19390
  this.checkNeedMoreData();
19309
19391
  this.scheduleRender();
19392
+ this.scheduleViewportPersist();
19310
19393
  },
19311
19394
  () => this.dataStore.length,
19312
19395
  () => this.scheduleRender(),
19313
19396
  () => this.drawingManager.getActiveTool() !== null || this.drawingManager.isDraggingDrawing() || this.drawingManager.isResizingDrawing() || this.separatorDrag !== null || this.axisDrag !== null || (this.tradeDragHandler?.active ?? false) || this._mobileCrosshairActive,
19314
19397
  (deltaY) => {
19398
+ if (this.priceScaleFactor === 1) return;
19315
19399
  const { priceH } = this.getChartLayout();
19316
19400
  const currentRange = this._displayedPriceRange ?? this.getScaledPriceRange(this._targetViewport);
19317
19401
  const pricePerPixel = (currentRange.max - currentRange.min) / priceH;
19318
19402
  this.pricePanOffset += deltaY * pricePerPixel;
19319
19403
  this.scheduleRender();
19404
+ this.scheduleViewportPersist();
19320
19405
  },
19321
19406
  () => {
19322
19407
  this._isPanning = true;
@@ -19339,6 +19424,7 @@ var _ChartEngine = class _ChartEngine {
19339
19424
  this.clearDuration();
19340
19425
  this.checkNeedMoreData();
19341
19426
  this.scheduleRender();
19427
+ this.scheduleViewportPersist();
19342
19428
  },
19343
19429
  () => this.dataStore.length,
19344
19430
  () => this.scheduleRender(),
@@ -19359,6 +19445,7 @@ var _ChartEngine = class _ChartEngine {
19359
19445
  this.clearDuration();
19360
19446
  this.checkNeedMoreData();
19361
19447
  this.scheduleRender();
19448
+ this.scheduleViewportPersist();
19362
19449
  }
19363
19450
  );
19364
19451
  this.drawCanvas.addEventListener("mousemove", this.onMouseMove);
@@ -19396,6 +19483,15 @@ var _ChartEngine = class _ChartEngine {
19396
19483
  return true;
19397
19484
  }).map((s) => s.indicator);
19398
19485
  }
19486
+ /** Schedules a stateChange emission so viewport/priceScale changes made by
19487
+ * pan/zoom/price-scale drag reach persistence. Coalesces rapid calls. */
19488
+ scheduleViewportPersist() {
19489
+ if (this._viewportPersistTimer) clearTimeout(this._viewportPersistTimer);
19490
+ this._viewportPersistTimer = setTimeout(() => {
19491
+ this._viewportPersistTimer = null;
19492
+ this.emitStateChange();
19493
+ }, 500);
19494
+ }
19399
19495
  get isFullscreen() {
19400
19496
  return this._isCssFullscreen || !!document.fullscreenElement;
19401
19497
  }
@@ -19487,6 +19583,22 @@ var _ChartEngine = class _ChartEngine {
19487
19583
  }
19488
19584
  this.recomputeIndicators();
19489
19585
  this.reanchorDrawingsByTimestamp();
19586
+ if (this._pendingViewportRestore) {
19587
+ const pending = this._pendingViewportRestore;
19588
+ this._pendingViewportRestore = null;
19589
+ if (pending.priceScale) {
19590
+ this.priceScaleFactor = pending.priceScale.factor;
19591
+ this.pricePanOffset = pending.priceScale.panOffset;
19592
+ this._displayedPriceRange = null;
19593
+ this._lockedAutoFitBase = null;
19594
+ }
19595
+ if (pending.viewport) {
19596
+ this.applyViewportFromTimestamps(
19597
+ pending.viewport.startTime,
19598
+ pending.viewport.endTime
19599
+ );
19600
+ }
19601
+ }
19490
19602
  this.updateCountdownState();
19491
19603
  this.scheduleRender();
19492
19604
  return this;
@@ -19507,6 +19619,7 @@ var _ChartEngine = class _ChartEngine {
19507
19619
  };
19508
19620
  this._targetViewport = { ...this.viewport };
19509
19621
  this.drawingManager.shiftBarIndices(n);
19622
+ this.reanchorDrawingsByTimestamp();
19510
19623
  this.recomputeIndicators();
19511
19624
  this.scheduleRender();
19512
19625
  }
@@ -20030,15 +20143,44 @@ var _ChartEngine = class _ChartEngine {
20030
20143
  ...d,
20031
20144
  points: d.points.map((p) => ({
20032
20145
  ...p,
20033
- timestamp: this.dataStore.all[Math.floor(p.barIndex)]?.time
20146
+ // Prefer the point's own timestamp (captured at placement / drag /
20147
+ // resize); only derive from current bars as a fallback for legacy
20148
+ // points. Re-deriving on every export would otherwise corrupt the
20149
+ // anchor after a timeframe change has staled the cached barIndex.
20150
+ timestamp: p.timestamp ?? this.barIndexToTimestamp(p.barIndex)
20034
20151
  }))
20035
20152
  })),
20036
20153
  positionRenderStyle: this.positionRenderStyle,
20037
20154
  tradeDisplayFilter: this.tradeDisplayFilter,
20038
20155
  canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
20039
- tfcEnabled: this.tfcActive
20156
+ tfcEnabled: this.tfcActive,
20157
+ viewport: this.getViewportTimestamps(),
20158
+ priceScale: this.priceScaleFactor !== 1 || this.pricePanOffset !== 0 ? { factor: this.priceScaleFactor, panOffset: this.pricePanOffset } : void 0
20040
20159
  };
20041
20160
  }
20161
+ /**
20162
+ * Captures the current viewport as a {startTime, endTime} timestamp pair,
20163
+ * preserving sub-bar fractional position. Returns undefined only when no
20164
+ * bars are loaded (viewport has no meaningful time anchor).
20165
+ */
20166
+ getViewportTimestamps() {
20167
+ if (this.dataStore.all.length === 0) return void 0;
20168
+ const startTime = this.barIndexToTimestamp(this.viewport.startIndex);
20169
+ const endTime = this.barIndexToTimestamp(this.viewport.endIndex);
20170
+ if (startTime == null || endTime == null) return void 0;
20171
+ return { startTime, endTime };
20172
+ }
20173
+ /**
20174
+ * Restores the viewport from a timestamp pair, converting each edge back
20175
+ * into a fractional bar index relative to the current dataStore. Callers
20176
+ * must ensure bars are loaded before invoking.
20177
+ */
20178
+ applyViewportFromTimestamps(startTime, endTime) {
20179
+ const startIndex = this.timestampToBarIndex(startTime);
20180
+ const endIndex = this.timestampToBarIndex(endTime);
20181
+ this.viewport = { ...this.viewport, startIndex, endIndex };
20182
+ this._targetViewport = { ...this.viewport };
20183
+ }
20042
20184
  /**
20043
20185
  * Restores chart configuration from a previously captured `ChartState`.
20044
20186
  * Call this after construction to hydrate a chart from persisted state.
@@ -20085,18 +20227,11 @@ var _ChartEngine = class _ChartEngine {
20085
20227
  }
20086
20228
  }
20087
20229
  }
20088
- const bars = this.dataStore.all;
20089
20230
  const resolvedDrawings = state.drawings.map((d) => ({
20090
20231
  ...d,
20091
20232
  points: d.points.map((p) => {
20092
- if (!p.timestamp || bars.length === 0) return p;
20093
- let lo = 0, hi = bars.length - 1;
20094
- while (lo < hi) {
20095
- const mid = lo + hi >> 1;
20096
- if ((bars[mid]?.time ?? 0) < p.timestamp) lo = mid + 1;
20097
- else hi = mid;
20098
- }
20099
- return { ...p, barIndex: lo + (p.barIndex - Math.floor(p.barIndex)) };
20233
+ if (p.timestamp == null || this.dataStore.all.length === 0) return p;
20234
+ return { ...p, barIndex: this.timestampToBarIndex(p.timestamp) };
20100
20235
  })
20101
20236
  }));
20102
20237
  this.drawingManager.loadDrawings(resolvedDrawings);
@@ -20114,6 +20249,25 @@ var _ChartEngine = class _ChartEngine {
20114
20249
  this.tfcActive = state.tfcEnabled;
20115
20250
  this.topBar?.setTfcActive(state.tfcEnabled);
20116
20251
  }
20252
+ if (state.priceScale) {
20253
+ this.priceScaleFactor = state.priceScale.factor;
20254
+ this.pricePanOffset = state.priceScale.panOffset;
20255
+ this._displayedPriceRange = null;
20256
+ this._lockedAutoFitBase = null;
20257
+ }
20258
+ if (state.viewport) {
20259
+ if (this.dataStore.all.length > 0) {
20260
+ this.applyViewportFromTimestamps(
20261
+ state.viewport.startTime,
20262
+ state.viewport.endTime
20263
+ );
20264
+ } else {
20265
+ this._pendingViewportRestore = {
20266
+ viewport: state.viewport,
20267
+ priceScale: state.priceScale
20268
+ };
20269
+ }
20270
+ }
20117
20271
  } finally {
20118
20272
  this.isRestoringState = false;
20119
20273
  }
@@ -20122,23 +20276,120 @@ var _ChartEngine = class _ChartEngine {
20122
20276
  return this;
20123
20277
  }
20124
20278
  /**
20125
- * Re-anchors all loaded drawings to the current dataset using their stored timestamps.
20126
- * Called after loadData() to fix drawings restored via setState() before data was
20127
- * available (bars.length === 0 at restore time causes the raw barIndex fallback).
20279
+ * Converts a fractional barIndex into an absolute unix-ms timestamp by
20280
+ * interpolating across the containing bar. Extrapolates linearly for indexes
20281
+ * outside [0, bars.length-1] using the adjacent bar's width.
20282
+ *
20283
+ * This is the inverse of `timestampToBarIndex` and the authoritative way to
20284
+ * capture "where in time" a drawing point sits at placement, drag, or resize.
20285
+ * Returns undefined only when no bars are loaded.
20286
+ */
20287
+ barIndexToTimestamp(idx) {
20288
+ const bars = this.dataStore.all;
20289
+ const n = bars.length;
20290
+ if (n === 0) return void 0;
20291
+ const first = bars[0];
20292
+ if (!first) return void 0;
20293
+ if (n === 1) return first.time;
20294
+ const floorIdx = Math.floor(idx);
20295
+ const frac = idx - floorIdx;
20296
+ if (floorIdx < 0) {
20297
+ const second = bars[1];
20298
+ if (!second) return first.time;
20299
+ return first.time + idx * (second.time - first.time);
20300
+ }
20301
+ if (floorIdx >= n - 1) {
20302
+ const last = bars[n - 1];
20303
+ const prev = bars[n - 2];
20304
+ if (!last || !prev) return first.time;
20305
+ return last.time + (idx - (n - 1)) * (last.time - prev.time);
20306
+ }
20307
+ const lo = bars[floorIdx];
20308
+ const hi = bars[floorIdx + 1];
20309
+ if (!lo || !hi) return first.time;
20310
+ return lo.time + frac * (hi.time - lo.time);
20311
+ }
20312
+ /**
20313
+ * Converts a unix-ms timestamp into a fractional barIndex using floor-bar
20314
+ * containment: the result is `floorBar + (ts - floorBar.time) / (nextBar.time - floorBar.time)`.
20315
+ * Extrapolates linearly for timestamps outside the loaded range using the edge
20316
+ * bar width, so drawings outside the visible data still render at the correct
20317
+ * relative offset rather than snapping to an edge.
20318
+ *
20319
+ * Matches TradingView's floor-bar semantics and preserves sub-bar time
20320
+ * precision across timeframe changes.
20321
+ */
20322
+ timestampToBarIndex(ts) {
20323
+ const bars = this.dataStore.all;
20324
+ const n = bars.length;
20325
+ if (n === 0) return 0;
20326
+ const first = bars[0];
20327
+ if (!first) return 0;
20328
+ if (n === 1) return 0;
20329
+ if (ts <= first.time) {
20330
+ const second = bars[1];
20331
+ if (!second) return 0;
20332
+ const barMs2 = second.time - first.time;
20333
+ return barMs2 > 0 ? (ts - first.time) / barMs2 : 0;
20334
+ }
20335
+ const lastIdx = n - 1;
20336
+ const lastBar = bars[lastIdx];
20337
+ const prevBar = bars[lastIdx - 1];
20338
+ if (!lastBar || !prevBar) return 0;
20339
+ if (ts >= lastBar.time) {
20340
+ const barMs2 = lastBar.time - prevBar.time;
20341
+ return lastIdx + (barMs2 > 0 ? (ts - lastBar.time) / barMs2 : 0);
20342
+ }
20343
+ let lo = 0, hi = lastIdx;
20344
+ while (lo < hi) {
20345
+ const mid = lo + hi + 1 >> 1;
20346
+ if ((bars[mid]?.time ?? 0) <= ts) lo = mid;
20347
+ else hi = mid - 1;
20348
+ }
20349
+ const loBar = bars[lo];
20350
+ const hiBar = bars[lo + 1];
20351
+ if (!loBar || !hiBar) return lo;
20352
+ const barMs = hiBar.time - loBar.time;
20353
+ return lo + (barMs > 0 ? (ts - loBar.time) / barMs : 0);
20354
+ }
20355
+ /**
20356
+ * Re-anchors all loaded drawings to the current dataset. For points with a
20357
+ * stored timestamp, barIndex is recomputed via `timestampToBarIndex`; for
20358
+ * points missing a timestamp (legacy saves or freshly placed points whose
20359
+ * timestamp capture was skipped), the timestamp is backfilled from the
20360
+ * current barIndex so subsequent data reloads remain accurate.
20361
+ *
20362
+ * Called after loadData(), prependData(), and whenever the underlying bars
20363
+ * change such that cached barIndex values would otherwise drift.
20128
20364
  */
20129
20365
  reanchorDrawingsByTimestamp() {
20130
20366
  const bars = this.dataStore.all;
20131
20367
  if (bars.length === 0) return;
20132
20368
  for (const drawing of this.drawingManager.getDrawings()) {
20133
20369
  for (const pt of drawing.points) {
20134
- if (!pt.timestamp) continue;
20135
- let lo = 0, hi = bars.length - 1;
20136
- while (lo < hi) {
20137
- const mid = lo + hi >> 1;
20138
- if ((bars[mid]?.time ?? 0) < pt.timestamp) lo = mid + 1;
20139
- else hi = mid;
20370
+ if (pt.timestamp == null) {
20371
+ const t = this.barIndexToTimestamp(pt.barIndex);
20372
+ if (t != null) pt.timestamp = t;
20373
+ continue;
20140
20374
  }
20141
- pt.barIndex = lo + (pt.barIndex - Math.floor(pt.barIndex));
20375
+ pt.barIndex = this.timestampToBarIndex(pt.timestamp);
20376
+ }
20377
+ }
20378
+ }
20379
+ /**
20380
+ * Captures a timestamp on any drawing point that is missing one, using the
20381
+ * current bars. Call this BEFORE mutating `dataStore.all` (insert/splice)
20382
+ * so that points still pointing at a stale barIndex get frozen to the
20383
+ * correct time before the mutation invalidates the index. Safe no-op for
20384
+ * points that already have a timestamp.
20385
+ */
20386
+ backfillDrawingTimestamps() {
20387
+ if (this.dataStore.all.length === 0) return;
20388
+ for (const drawing of this.drawingManager.getDrawings()) {
20389
+ for (const pt of drawing.points) {
20390
+ if (pt.timestamp != null) continue;
20391
+ const t = this.barIndexToTimestamp(pt.barIndex);
20392
+ if (t != null) pt.timestamp = t;
20142
20393
  }
20143
20394
  }
20144
20395
  }
@@ -20168,6 +20419,7 @@ var _ChartEngine = class _ChartEngine {
20168
20419
  }
20169
20420
  setSymbol(symbol) {
20170
20421
  this.symbol = symbol;
20422
+ this.tradePopover?.setSymbol(symbol);
20171
20423
  if (this.siSymbolSpan) {
20172
20424
  this.siSymbolSpan.textContent = symbol;
20173
20425
  this.siSymbolSpan.style.display = symbol ? "inline" : "none";
@@ -21175,7 +21427,7 @@ var _ChartEngine = class _ChartEngine {
21175
21427
  return;
21176
21428
  }
21177
21429
  const { chartW, priceH } = this.getChartLayout();
21178
- const priceRange = this.getScaledPriceRange(viewport);
21430
+ const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(viewport);
21179
21431
  const bracketOrders = this.levels.filter(
21180
21432
  (l) => l.type === "pending" && l.ToClose != null
21181
21433
  );
@@ -21240,7 +21492,12 @@ var _ChartEngine = class _ChartEngine {
21240
21492
  this.tradeDragHandler?.dragLabel ?? null,
21241
21493
  this.tradeDragNewPrice,
21242
21494
  this.selectedTradeLabel,
21243
- new Set(this.pendingLevelDrags.keys()),
21495
+ // Include _editOpenLabel so confirm/cancel buttons appear while the edit form is open
21496
+ // even before any change is staged — e.g. editing a position with no SL/TP.
21497
+ /* @__PURE__ */ new Set([
21498
+ ...this.pendingLevelDrags.keys(),
21499
+ ...this._editOpenLabel !== null ? [this._editOpenLabel] : []
21500
+ ]),
21244
21501
  this.positionRenderStyle,
21245
21502
  this.hideLevelConfirmCancel,
21246
21503
  this.draftOrderLabel,
@@ -21443,6 +21700,11 @@ var _ChartEngine = class _ChartEngine {
21443
21700
  }
21444
21701
  }
21445
21702
  }
21703
+ if (result.newPrice === oldPrice) {
21704
+ this.tradeDragNewPrice = null;
21705
+ this.renderTradeLayer();
21706
+ return;
21707
+ }
21446
21708
  const field = result.bracketType ?? "main";
21447
21709
  const isNew = this._isBracketNew;
21448
21710
  this._isBracketNew = false;
@@ -21548,7 +21810,22 @@ var _ChartEngine = class _ChartEngine {
21548
21810
  }
21549
21811
  applyPendingChanges(label) {
21550
21812
  const changes = this.pendingLevelDrags.get(label);
21551
- if (!changes) return;
21813
+ if (!changes) {
21814
+ if (this._editOpenLabel === label) {
21815
+ const level2 = this.levels.find((l) => l.label === label);
21816
+ this._editOpenLabel = null;
21817
+ this.selectedTradeLabel = null;
21818
+ this.renderTradeLayer();
21819
+ if (level2) {
21820
+ this.emitter.emit("tradeLevelConfirmed", {
21821
+ label,
21822
+ type: level2.type,
21823
+ isFullscreen: this.isFullscreen
21824
+ });
21825
+ }
21826
+ }
21827
+ return;
21828
+ }
21552
21829
  const level = this.levels.find((l) => l.label === label);
21553
21830
  if (!level) return;
21554
21831
  if (this._editOpenLabel === label) this._editOpenLabel = null;
@@ -21627,6 +21904,10 @@ var _ChartEngine = class _ChartEngine {
21627
21904
  if (!label && this.draftOrderLabel) {
21628
21905
  this.removeDraftOrder();
21629
21906
  }
21907
+ if (label && !this.pendingLevelDrags.has(label) && this._editOpenLabel === label) {
21908
+ this._cancelEditAndDeselect(label);
21909
+ return;
21910
+ }
21630
21911
  const toRevert = label ? this.pendingLevelDrags.has(label) ? [[label, this.pendingLevelDrags.get(label) ?? []]] : [] : [...this.pendingLevelDrags.entries()];
21631
21912
  for (const [lbl] of toRevert) {
21632
21913
  this.undoIsNewBracketMutations(lbl);
@@ -22199,6 +22480,7 @@ var _ChartEngine = class _ChartEngine {
22199
22480
  this.leftBar?.setMobileMode(isMobile);
22200
22481
  this.topBar?.setMobileLayout(isMobile);
22201
22482
  this.tradeButton?.setMobileMode(isMobile);
22483
+ this.tradePopover?.setCompact(isMobile);
22202
22484
  if (this.siEl) {
22203
22485
  this.siEl.style.flexDirection = isMobile ? "column" : "row";
22204
22486
  this.siEl.style.alignItems = isMobile ? "flex-start" : "center";
@@ -22289,10 +22571,21 @@ var _ChartEngine = class _ChartEngine {
22289
22571
  const y = touch.clientY - rect.top;
22290
22572
  const { chartW, priceH } = this.getChartLayout();
22291
22573
  const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
22292
- return {
22293
- barIndex: this.scaleManager.pixelToBarIndex(x, this.viewport, chartW),
22294
- price: this.scaleManager.pixelToPrice(y, priceRange, priceH)
22295
- };
22574
+ return this.makeDrawingPoint(
22575
+ this.scaleManager.pixelToBarIndex(x, this.viewport, chartW),
22576
+ this.scaleManager.pixelToPrice(y, priceRange, priceH)
22577
+ );
22578
+ }
22579
+ /**
22580
+ * Builds a fully-populated DrawingPoint from raw float coordinates. Captures
22581
+ * the absolute timestamp at this fractional barIndex so the anchor survives
22582
+ * timeframe changes, data reloads, and serialization round-trips.
22583
+ */
22584
+ makeDrawingPoint(barIndex, price) {
22585
+ const point = { barIndex, price };
22586
+ const t = this.barIndexToTimestamp(barIndex);
22587
+ if (t != null) point.timestamp = t;
22588
+ return point;
22296
22589
  }
22297
22590
  /** Shared close-button handler used by both mouse click and touch tap. */
22298
22591
  _handleTradeLevelClose(closedLevel) {
@@ -22402,6 +22695,49 @@ var _ChartEngine = class _ChartEngine {
22402
22695
  }
22403
22696
  this.scheduleRender();
22404
22697
  }
22698
+ /**
22699
+ * Closes the edit form, clears selection, and emits `tradeLevelEditCancelled` when
22700
+ * the edit form was actually open for this label. Shared between click-outside,
22701
+ * repeat-click toggle, and the ✗ button so all exit paths produce the same payload.
22702
+ */
22703
+ _cancelEditAndDeselect(label) {
22704
+ const wasEditFormOpen = this._editOpenLabel === label;
22705
+ const level = this.levels.find((l) => l.label === label);
22706
+ this._editOpenLabel = null;
22707
+ this.selectedTradeLabel = null;
22708
+ this.draftBracketPnl = {};
22709
+ this.renderTradeLayer();
22710
+ if (wasEditFormOpen && level) {
22711
+ this.emitter.emit("tradeLevelEditCancelled", {
22712
+ label,
22713
+ type: level.type,
22714
+ isFullscreen: this.isFullscreen
22715
+ });
22716
+ }
22717
+ }
22718
+ /**
22719
+ * Opens the external edit form for a trade level by emitting `tradeLevelEditOpen`.
22720
+ * Shared between pencil click, bare box click, and drag start so every entry
22721
+ * point fires the same payload. No-op when the form is already open for this
22722
+ * label, so repeat opens (e.g. pencil click followed by drag) don't spam the event.
22723
+ */
22724
+ _openTradeLevelEditForm(level) {
22725
+ if (this._editOpenLabel === level.label) return;
22726
+ this.selectedTradeLabel = level.label;
22727
+ this._editOpenLabel = level.label;
22728
+ const { slPrice, tpPrice } = this._resolveBracketPrices(level);
22729
+ this.renderTradeLayer();
22730
+ this.emitter.emit("tradeLevelEditOpen", {
22731
+ label: level.label,
22732
+ type: level.type,
22733
+ data: level.data,
22734
+ price: level.price,
22735
+ side: level.side,
22736
+ stopLossPrice: slPrice,
22737
+ takeProfitPrice: tpPrice,
22738
+ isFullscreen: this.isFullscreen
22739
+ });
22740
+ }
22405
22741
  /** Shared helper: initiate a trade level main-line drag from a hit area. */
22406
22742
  _startTradeDragFromHit(dragHit, level, hitY) {
22407
22743
  if (!this.tradeDragHandler) return;
@@ -22477,6 +22813,7 @@ var _ChartEngine = class _ChartEngine {
22477
22813
  );
22478
22814
  }
22479
22815
  }
22816
+ this._openTradeLevelEditForm(level);
22480
22817
  }
22481
22818
  /** Shared helper: initiate a +SL/+TP add-bracket drag from a hit area. */
22482
22819
  _startAddBracketFromHit(addHit, hitY) {
@@ -22590,6 +22927,7 @@ var _ChartEngine = class _ChartEngine {
22590
22927
  this.dataLoader({ start: new Date(fromTime), end: new Date(toTime), interval }).then((rawBars) => {
22591
22928
  const bars = this.maybeAggregate(rawBars);
22592
22929
  if (bars.length === 0) return;
22930
+ this.backfillDrawingTimestamps();
22593
22931
  const all = this.dataStore.all;
22594
22932
  const liveBar = all[all.length - 1];
22595
22933
  let insertCount = 0;
@@ -22610,6 +22948,7 @@ var _ChartEngine = class _ChartEngine {
22610
22948
  startIndex: this.viewport.startIndex + insertCount,
22611
22949
  endIndex: this.viewport.endIndex + insertCount
22612
22950
  };
22951
+ this.reanchorDrawingsByTimestamp();
22613
22952
  this.recomputeIndicators();
22614
22953
  this.scheduleRender();
22615
22954
  }
@@ -22804,6 +23143,7 @@ var _ChartEngine = class _ChartEngine {
22804
23143
  }
22805
23144
  };
22806
23145
  _ChartEngine.DRAW_GESTURE_THRESHOLD = 4;
23146
+ _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD = 3;
22807
23147
  var ChartEngine = _ChartEngine;
22808
23148
 
22809
23149
  // src/data/WebSocketAdapter.ts