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.js CHANGED
@@ -265,6 +265,8 @@ function resolveCssVarsInTheme(theme, container) {
265
265
  dropdownHoverBg: resolve(theme.indicatorOverlay.dropdownHoverBg)
266
266
  },
267
267
  tradeLevels: {
268
+ buySide: resolve(theme.tradeLevels.buySide),
269
+ sellSide: resolve(theme.tradeLevels.sellSide),
268
270
  tradeLine: resolve(theme.tradeLevels.tradeLine),
269
271
  positionLine: resolve(theme.tradeLevels.positionLine),
270
272
  pendingBuyLine: resolve(theme.tradeLevels.pendingBuyLine),
@@ -341,6 +343,8 @@ var DARK_THEME = {
341
343
  dropdownHoverBg: "#1c2a35"
342
344
  },
343
345
  tradeLevels: {
346
+ buySide: "#26a69a",
347
+ sellSide: "#ef5350",
344
348
  tradeLine: "#5caf79",
345
349
  positionLine: "#7b61ff",
346
350
  pendingBuyLine: "rgba(92,175,121,0.75)",
@@ -413,6 +417,8 @@ var LIGHT_THEME = {
413
417
  dropdownHoverBg: "#f0f0f0"
414
418
  },
415
419
  tradeLevels: {
420
+ buySide: "#26a69a",
421
+ sellSide: "#ef5350",
416
422
  tradeLine: "#5caf79",
417
423
  positionLine: "#6d28d9",
418
424
  pendingBuyLine: "rgba(92,175,121,0.75)",
@@ -698,6 +704,7 @@ var DEFAULT_LABELS = {
698
704
  limit: "Limit",
699
705
  stop: "Stop",
700
706
  market: "Market",
707
+ at: "at",
701
708
  qty: "Qty",
702
709
  placeOrderTitle: "Place order at this price"
703
710
  },
@@ -1673,16 +1680,18 @@ function computeFanOutLayout(cluster, boxH, gapPx = 4) {
1673
1680
  return result;
1674
1681
  }
1675
1682
  function buildClusterBadgeLayout(cluster, chartW, ctx) {
1676
- const n = cluster.members.length;
1677
- const types = new Set(cluster.members.map((m) => m.level.type));
1678
- let noun;
1679
- if (types.size === 1) {
1680
- const t = types.values().next().value;
1681
- noun = t === "position" ? "Position" : t === "pending" ? "Order" : "Trade";
1682
- } else {
1683
- noun = "Level";
1684
- }
1685
- const text = `${n} ${noun}${n > 1 ? "s" : ""}`;
1683
+ const posCount = cluster.members.filter(
1684
+ (m) => m.level.type === "position" && !m.level.entryPriceEditable
1685
+ ).length;
1686
+ const orderCount = cluster.members.filter(
1687
+ (m) => m.level.type === "pending" || m.level.entryPriceEditable === true
1688
+ ).length;
1689
+ const tradeCount = cluster.members.filter((m) => m.level.type === "trade").length;
1690
+ const parts = [];
1691
+ if (posCount > 0) parts.push(`${posCount} Position${posCount > 1 ? "s" : ""}`);
1692
+ if (orderCount > 0) parts.push(`${orderCount} Order${orderCount > 1 ? "s" : ""}`);
1693
+ if (tradeCount > 0) parts.push(`${tradeCount} Trade${tradeCount > 1 ? "s" : ""}`);
1694
+ const text = parts.join(" / ");
1686
1695
  const PAD_X = 12;
1687
1696
  const textW = ctx.measureText(text).width;
1688
1697
  const EXPAND_ICON_W = 16;
@@ -2090,6 +2099,22 @@ function renderTradeLevels(ctx, levels, scale, priceRange, theme, chartW, priceH
2090
2099
  const offMainDragActive = draggingLabel === offLevel.label;
2091
2100
  const offSlPrice = offMainDragActive && dragNewSLPrice !== null ? dragNewSLPrice : bh.stopLossPrice;
2092
2101
  const offTpPrice = offMainDragActive && dragNewTPPrice !== null ? dragNewTPPrice : bh.takeProfitPrice;
2102
+ if (offSlPrice !== void 0 && offTpPrice !== void 0) {
2103
+ const slY = scale.yToPixel(offSlPrice, priceRange, priceH);
2104
+ const tpY = scale.yToPixel(offTpPrice, priceRange, priceH);
2105
+ const bracketBH = Math.max(BRACKET_BOX_H, CLOSE_R * 2 + BRACKET_BOX_PAD_Y * 2);
2106
+ ctx.save();
2107
+ ctx.font = FONT_BRACKET;
2108
+ const closeSlot = CLOSE_R * 2 + BRACKET_BOX_PAD_X / 2;
2109
+ const slBW = BRACKET_BOX_PAD_X + ctx.measureText(`SL ${offSlPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2110
+ const tpBW = BRACKET_BOX_PAD_X + ctx.measureText(`TP ${offTpPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2111
+ ctx.restore();
2112
+ const connX = ((bracketLineXMap.get(slDragKey) ?? fallbackX) - slBW / 2 - 2 + ((bracketLineXMap.get(tpDragKey) ?? fallbackX) - tpBW / 2 - 2)) / 2;
2113
+ const slPillY = bracketBoxYMap.get(slDragKey) ?? slY - bracketBH / 2;
2114
+ const tpPillY = bracketBoxYMap.get(tpDragKey) ?? tpY - bracketBH / 2;
2115
+ const [upperY, lowerY] = tpPillY < slPillY ? [tpPillY, slPillY] : [slPillY, tpPillY];
2116
+ drawSLTPConnector(ctx, connX, upperY + bracketBH, lowerY, theme);
2117
+ }
2093
2118
  if (offSlPrice !== void 0) {
2094
2119
  drawBracketLine(
2095
2120
  ctx,
@@ -2223,6 +2248,20 @@ function drawFanConnector(ctx, fanCenterY, realY, chartW, theme) {
2223
2248
  ctx.stroke();
2224
2249
  ctx.restore();
2225
2250
  }
2251
+ function drawSLTPConnector(ctx, x, yTop, yBottom, theme) {
2252
+ if (yBottom - yTop < 2) return;
2253
+ ctx.save();
2254
+ ctx.strokeStyle = monoColor(theme);
2255
+ ctx.globalAlpha = 0.5;
2256
+ ctx.lineWidth = 1;
2257
+ ctx.setLineDash([3, 4]);
2258
+ ctx.beginPath();
2259
+ ctx.moveTo(x, yTop);
2260
+ ctx.lineTo(x, yBottom);
2261
+ ctx.stroke();
2262
+ ctx.setLineDash([]);
2263
+ ctx.restore();
2264
+ }
2226
2265
  function drawDotHoverBox(ctx, level, y, dotX, dotR, theme, precision) {
2227
2266
  const pl = level;
2228
2267
  const rows = [];
@@ -2334,6 +2373,24 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
2334
2373
  const mainDragActive = draggingLabel === level.label;
2335
2374
  const slRenderPrice = mainDragActive && dragNewSLPrice !== null ? dragNewSLPrice : bracketHost.stopLossPrice;
2336
2375
  const tpRenderPrice = mainDragActive && dragNewTPPrice !== null ? dragNewTPPrice : bracketHost.takeProfitPrice;
2376
+ if (selected && slRenderPrice !== void 0 && tpRenderPrice !== void 0) {
2377
+ const slY = scale.yToPixel(slRenderPrice, priceRange, priceH);
2378
+ const tpY = scale.yToPixel(tpRenderPrice, priceRange, priceH);
2379
+ const bracketBH = hideConfirmCancel ? BRACKET_BOX_H : Math.max(BRACKET_BOX_H, closeR * 2 + BRACKET_BOX_PAD_Y * 2);
2380
+ ctx.save();
2381
+ ctx.font = FONT_BRACKET;
2382
+ const closeSlot = hideConfirmCancel ? 0 : closeR * 2 + BRACKET_BOX_PAD_X / 2;
2383
+ const slBW = BRACKET_BOX_PAD_X + ctx.measureText(`SL ${slRenderPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2384
+ const tpBW = BRACKET_BOX_PAD_X + ctx.measureText(`TP ${tpRenderPrice.toFixed(precision)}`).width + BRACKET_BOX_PAD_X + closeSlot;
2385
+ ctx.restore();
2386
+ const slBLineX = bracketLineXMap.get(slDragKey) ?? chartW * BRACKET_BASE_RATIO;
2387
+ const tpBLineX = bracketLineXMap.get(tpDragKey) ?? chartW * BRACKET_BASE_RATIO;
2388
+ const connX = (slBLineX - slBW / 2 - 2 + (tpBLineX - tpBW / 2 - 2)) / 2;
2389
+ const slPillY = bracketBoxYMap?.get(slDragKey) ?? slY - bracketBH / 2;
2390
+ const tpPillY = bracketBoxYMap?.get(tpDragKey) ?? tpY - bracketBH / 2;
2391
+ const [upperY, lowerY] = tpPillY < slPillY ? [tpPillY, slPillY] : [slPillY, tpPillY];
2392
+ drawSLTPConnector(ctx, connX, upperY + bracketBH, lowerY, theme);
2393
+ }
2337
2394
  if (slRenderPrice !== void 0) {
2338
2395
  const bLineX = bracketLineXMap.get(slDragKey) ?? chartW * BRACKET_BASE_RATIO;
2339
2396
  drawBracketLine(
@@ -11332,6 +11389,14 @@ var DrawingManager = class {
11332
11389
  this.onDrawingsChange = null;
11333
11390
  /** Called when a text-type drawing is first completed — ChartEngine shows the text input. */
11334
11391
  this.onStartTextEdit = null;
11392
+ /**
11393
+ * Called after drag/resize mutates a drawing's points so ChartEngine can
11394
+ * refresh each point's `timestamp` from the current dataStore. Required
11395
+ * because DrawingManager has no access to the data layer, and drag
11396
+ * reconstructs points from raw (barIndex, price) deltas that strip the
11397
+ * original point's timestamp.
11398
+ */
11399
+ this.onPointsMutated = null;
11335
11400
  /** Set to true by ChartEngine while the text input is focused — prevents Delete/Backspace from deleting the drawing. */
11336
11401
  this.editingText = false;
11337
11402
  // Drag state (move whole drawing)
@@ -11426,14 +11491,18 @@ var DrawingManager = class {
11426
11491
  const dBar = point.barIndex - this.dragStartMouse.barIndex;
11427
11492
  const dPrice = point.price - this.dragStartMouse.price;
11428
11493
  for (let i = 0; i < this.dragDrawing.points.length; i++) {
11494
+ const start = this.dragStartPoints[i];
11495
+ if (!start) continue;
11429
11496
  this.dragDrawing.points[i] = {
11430
- barIndex: this.dragStartPoints[i].barIndex + dBar,
11431
- price: this.dragStartPoints[i].price + dPrice
11497
+ barIndex: start.barIndex + dBar,
11498
+ price: start.price + dPrice
11432
11499
  };
11433
11500
  }
11501
+ this.onPointsMutated?.(this.dragDrawing);
11434
11502
  }
11435
11503
  if (this.resizeDrawing && this.resizePointIndex >= 0) {
11436
11504
  this.resizeDrawing.points[this.resizePointIndex] = { ...point };
11505
+ this.onPointsMutated?.(this.resizeDrawing);
11437
11506
  }
11438
11507
  }
11439
11508
  /**
@@ -13190,8 +13259,8 @@ var TradeButton = class {
13190
13259
  this.el.style.opacity = "1";
13191
13260
  Object.assign(this.el.style, {
13192
13261
  background: theme.tooltip.background,
13193
- color: "#26a69a",
13194
- border: "1px solid #26a69a"
13262
+ color: theme.tradeLevels.buySide,
13263
+ border: `1px solid ${theme.tradeLevels.buySide}`
13195
13264
  });
13196
13265
  });
13197
13266
  this.el.addEventListener("mouseleave", (e) => {
@@ -13254,11 +13323,12 @@ var TradeButton = class {
13254
13323
 
13255
13324
  // src/ui/TradePopover.ts
13256
13325
  var TradePopover = class {
13257
- constructor(theme, lots, onDraftCreate, labels = DEFAULT_LABELS.trade, onVisibilityChange) {
13326
+ constructor(theme, lots, symbol, onDraftCreate, labels = DEFAULT_LABELS.trade, onVisibilityChange) {
13258
13327
  this._price = 0;
13259
13328
  this._pricePrecision = 2;
13260
13329
  this._buyOrderType = "limit";
13261
13330
  this._sellOrderType = "limit";
13331
+ this._compact = false;
13262
13332
  this.onDocMouseDown = (e) => {
13263
13333
  if (this.isVisible && !this.el.contains(e.target)) {
13264
13334
  this.hide();
@@ -13266,6 +13336,7 @@ var TradePopover = class {
13266
13336
  };
13267
13337
  this.theme = theme;
13268
13338
  this.lots = lots;
13339
+ this.symbol = symbol;
13269
13340
  this.onDraftCreate = onDraftCreate;
13270
13341
  this.labels = labels;
13271
13342
  this.onVisibilityChange = onVisibilityChange;
@@ -13274,7 +13345,6 @@ var TradePopover = class {
13274
13345
  position: "absolute",
13275
13346
  display: "none",
13276
13347
  flexDirection: "column",
13277
- gap: "1px",
13278
13348
  background: theme.tooltip.background,
13279
13349
  border: `1px solid ${theme.axisBorder}`,
13280
13350
  borderRadius: "5px",
@@ -13283,7 +13353,7 @@ var TradePopover = class {
13283
13353
  boxShadow: "0 4px 16px rgba(0,0,0,0.25)",
13284
13354
  fontFamily: "'Inter', system-ui, sans-serif",
13285
13355
  fontSize: "12px",
13286
- minWidth: "140px",
13356
+ width: "max-content",
13287
13357
  pointerEvents: "auto"
13288
13358
  });
13289
13359
  document.addEventListener("mousedown", this.onDocMouseDown);
@@ -13297,10 +13367,8 @@ var TradePopover = class {
13297
13367
  this._sellOrderType = sellOrderType;
13298
13368
  this.rebuild();
13299
13369
  this.el.style.display = "flex";
13300
- const popW = 145;
13301
13370
  this.el.style.right = `${buttonRight}px`;
13302
13371
  this.el.style.top = `${buttonY - 16}px`;
13303
- this.el.style.width = `${popW}px`;
13304
13372
  if (!wasVisible) this.onVisibilityChange?.();
13305
13373
  }
13306
13374
  hide() {
@@ -13314,6 +13382,12 @@ var TradePopover = class {
13314
13382
  setLots(lots) {
13315
13383
  this.lots = lots;
13316
13384
  }
13385
+ setSymbol(symbol) {
13386
+ this.symbol = symbol;
13387
+ }
13388
+ setCompact(compact) {
13389
+ this._compact = compact;
13390
+ }
13317
13391
  setTheme(theme) {
13318
13392
  this.theme = theme;
13319
13393
  Object.assign(this.el.style, {
@@ -13327,31 +13401,46 @@ var TradePopover = class {
13327
13401
  // ── Private ───────────────────────────────────────────────────────────────
13328
13402
  rebuild() {
13329
13403
  this.el.innerHTML = "";
13404
+ const divider = document.createElement("div");
13405
+ Object.assign(divider.style, {
13406
+ height: "1px",
13407
+ background: this.theme.axisBorder,
13408
+ margin: "0 8px"
13409
+ });
13330
13410
  this.el.append(
13331
- this.makeRow(this.labels.buy, "#26a69a", "buy", this._buyOrderType),
13332
- this.makeRow(this.labels.sell, "#ef5350", "sell", this._sellOrderType)
13411
+ this.makeRow(this.theme.tradeLevels.sellSide, "sell", this._sellOrderType),
13412
+ divider,
13413
+ this.makeRow(this.theme.tradeLevels.buySide, "buy", this._buyOrderType)
13333
13414
  );
13334
13415
  }
13335
- makeRow(label, color, side, orderType) {
13416
+ makeRow(color, side, orderType) {
13336
13417
  const btn = document.createElement("button");
13337
- const priceStr = this._price.toFixed(this._pricePrecision);
13338
- btn.textContent = `${label} ${orderType === "limit" ? this.labels.limit : this.labels.stop} ${priceStr}`;
13339
13418
  Object.assign(btn.style, {
13340
- display: "block",
13419
+ display: "flex",
13420
+ alignItems: "center",
13421
+ gap: "8px",
13341
13422
  width: "100%",
13342
13423
  background: "transparent",
13343
- color,
13344
13424
  border: "none",
13345
13425
  borderRadius: "3px",
13346
- padding: "7px 10px",
13347
- textAlign: "left",
13426
+ padding: this._compact ? "5px 8px" : "8px 10px",
13348
13427
  cursor: "pointer",
13349
- fontSize: "12px",
13350
13428
  fontFamily: "'Inter', system-ui, sans-serif",
13351
- fontWeight: "600"
13429
+ fontSize: this._compact ? "11px" : "12px"
13352
13430
  });
13431
+ btn.appendChild(this.makeChevron(color, side));
13432
+ const sideLabel = side === "sell" ? this.labels.sell : this.labels.buy;
13433
+ const orderTypeLabel = orderType === "limit" ? this.labels.limit : this.labels.stop;
13434
+ const priceStr = this._price.toFixed(this._pricePrecision);
13435
+ const colored = document.createElement("span");
13436
+ colored.textContent = `${sideLabel} ${orderTypeLabel}`;
13437
+ Object.assign(colored.style, { color, fontWeight: "600" });
13438
+ const plain = document.createElement("span");
13439
+ plain.textContent = ` ${this.lots} ${this.symbol} ${this.labels.at} ${priceStr}`;
13440
+ Object.assign(plain.style, { color: "#ffffff", fontWeight: "400" });
13441
+ btn.append(colored, plain);
13353
13442
  btn.addEventListener("mouseenter", () => {
13354
- btn.style.background = color + "22";
13443
+ btn.style.background = "rgba(255,255,255,0.06)";
13355
13444
  });
13356
13445
  btn.addEventListener("mouseleave", () => {
13357
13446
  btn.style.background = "transparent";
@@ -13363,6 +13452,23 @@ var TradePopover = class {
13363
13452
  });
13364
13453
  return btn;
13365
13454
  }
13455
+ makeChevron(color, side) {
13456
+ const ns = "http://www.w3.org/2000/svg";
13457
+ const svg = document.createElementNS(ns, "svg");
13458
+ svg.setAttribute("width", "10");
13459
+ svg.setAttribute("height", "8");
13460
+ svg.setAttribute("viewBox", "0 0 10 8");
13461
+ svg.style.flexShrink = "0";
13462
+ const path = document.createElementNS(ns, "path");
13463
+ path.setAttribute("d", side === "sell" ? "M1 1 L5 7 L9 1" : "M1 7 L5 1 L9 7");
13464
+ path.setAttribute("stroke", color);
13465
+ path.setAttribute("fill", "none");
13466
+ path.setAttribute("stroke-width", "1.5");
13467
+ path.setAttribute("stroke-linecap", "round");
13468
+ path.setAttribute("stroke-linejoin", "round");
13469
+ svg.appendChild(path);
13470
+ return svg;
13471
+ }
13366
13472
  };
13367
13473
 
13368
13474
  // src/ui/DraftQtyFlyout.ts
@@ -15064,6 +15170,13 @@ var _ChartEngine = class _ChartEngine {
15064
15170
  /** Pending resize — committed only after mouse moves past DRAW_GESTURE_THRESHOLD. */
15065
15171
  this._pendingDrawResize = null;
15066
15172
  // px
15173
+ /**
15174
+ * Pending trade-level drag — committed only after mouse moves past TRADE_DRAG_PIXEL_THRESHOLD.
15175
+ * A mouseup before the threshold means it was a click: route to _openTradeLevelEditForm instead
15176
+ * of finishing a zero-delta drag (which would otherwise stage a spurious pending change).
15177
+ */
15178
+ this._pendingTradeDrag = null;
15179
+ // px
15067
15180
  // Pane resize
15068
15181
  this.paneHeightOverrides = /* @__PURE__ */ new Map();
15069
15182
  this.separatorDrag = null;
@@ -15079,6 +15192,21 @@ var _ChartEngine = class _ChartEngine {
15079
15192
  // lerp target; null = snap on next render
15080
15193
  this._lockedAutoFitBase = null;
15081
15194
  // non-null = user has manually adjusted Y-axis
15195
+ /**
15196
+ * When setState() restores a persisted viewport but data isn't loaded yet,
15197
+ * we stash the desired time window here and apply it from loadData() once
15198
+ * bars are available. Cleared after one application.
15199
+ */
15200
+ this._pendingViewportRestore = null;
15201
+ /**
15202
+ * Debounces viewport/priceScale stateChange emissions during continuous
15203
+ * gestures (pan, wheel-zoom, price-scale drag). Pan/zoom don't emit
15204
+ * stateChange directly because doing so per-frame would be wasteful; this
15205
+ * timer coalesces bursts so the persisted viewport catches up a few ms
15206
+ * after the user stops interacting. Paired with the consumer's own
15207
+ * debounce (e.g. CRM's 800ms save) for minimal traffic.
15208
+ */
15209
+ this._viewportPersistTimer = null;
15082
15210
  this.axisDrag = null;
15083
15211
  this.subPaneScaleFactors = /* @__PURE__ */ new Map();
15084
15212
  this._mobileCrosshairActive = false;
@@ -15239,14 +15367,14 @@ var _ChartEngine = class _ChartEngine {
15239
15367
  const dx = x - this._pendingDrawDrag.startX;
15240
15368
  const dy = y - this._pendingDrawDrag.startY;
15241
15369
  if (Math.sqrt(dx * dx + dy * dy) > _ChartEngine.DRAW_GESTURE_THRESHOLD) {
15242
- this.drawingManager.startDrag(this._pendingDrawDrag.drawing, {
15243
- barIndex: rawBarIndex,
15244
- price
15245
- });
15370
+ this.drawingManager.startDrag(
15371
+ this._pendingDrawDrag.drawing,
15372
+ this.makeDrawingPoint(rawBarIndex, price)
15373
+ );
15246
15374
  this._pendingDrawDrag = null;
15247
15375
  }
15248
15376
  }
15249
- this.drawingManager.onMouseMove({ barIndex: rawBarIndex, price });
15377
+ this.drawingManager.onMouseMove(this.makeDrawingPoint(rawBarIndex, price));
15250
15378
  if (this.axisDrag) {
15251
15379
  if (this.axisDrag.axis === "price") {
15252
15380
  const delta = y - this.axisDrag.startPos;
@@ -15288,6 +15416,18 @@ var _ChartEngine = class _ChartEngine {
15288
15416
  this.scheduleRender();
15289
15417
  return;
15290
15418
  }
15419
+ if (this._pendingTradeDrag) {
15420
+ const dx = x - this._pendingTradeDrag.startX;
15421
+ const dy = y - this._pendingTradeDrag.startY;
15422
+ if (Math.sqrt(dx * dx + dy * dy) > _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD) {
15423
+ const { dragHit, level, hitY } = this._pendingTradeDrag;
15424
+ this._pendingTradeDrag = null;
15425
+ this._startTradeDragFromHit(dragHit, level, hitY);
15426
+ this.drawCanvas.style.cursor = "ns-resize";
15427
+ } else {
15428
+ return;
15429
+ }
15430
+ }
15291
15431
  if (this.tradeDragHandler?.active) {
15292
15432
  const moveResult = this.tradeDragHandler.onMouseMove(
15293
15433
  y,
@@ -15328,6 +15468,10 @@ var _ChartEngine = class _ChartEngine {
15328
15468
  const dragHover = hitTestTradeLevelDrag(x, y, this.tradeLevelDragAreas);
15329
15469
  if (dragHover) {
15330
15470
  this.drawCanvas.style.cursor = "ns-resize";
15471
+ if (dragHover.label !== this.hoveredTradeLabel) {
15472
+ this.hoveredTradeLabel = dragHover.label;
15473
+ this.renderTradeLayer();
15474
+ }
15331
15475
  this.scheduleRender();
15332
15476
  return;
15333
15477
  }
@@ -15397,6 +15541,16 @@ var _ChartEngine = class _ChartEngine {
15397
15541
  this.scheduleRender();
15398
15542
  };
15399
15543
  this.onMouseUp = () => {
15544
+ if (this._pendingTradeDrag) {
15545
+ const { level } = this._pendingTradeDrag;
15546
+ this._pendingTradeDrag = null;
15547
+ if (this._editOpenLabel === level.label && !this.pendingLevelDrags.has(level.label)) {
15548
+ this._cancelEditAndDeselect(level.label);
15549
+ return;
15550
+ }
15551
+ this._openTradeLevelEditForm(level);
15552
+ return;
15553
+ }
15400
15554
  if (this.tradeDragHandler?.active) {
15401
15555
  const result = this.tradeDragHandler.endDrag();
15402
15556
  if (result) {
@@ -15413,9 +15567,11 @@ var _ChartEngine = class _ChartEngine {
15413
15567
  return;
15414
15568
  }
15415
15569
  if (this.axisDrag) {
15570
+ const wasPriceAxis = this.axisDrag.axis === "price" || this.axisDrag.axis === "time";
15416
15571
  this.axisDrag = null;
15417
15572
  this.drawCanvas.style.cursor = "default";
15418
15573
  this.scheduleRender();
15574
+ if (wasPriceAxis) this.scheduleViewportPersist();
15419
15575
  }
15420
15576
  if (this.separatorDrag) {
15421
15577
  this.separatorDrag = null;
@@ -15577,22 +15733,7 @@ var _ChartEngine = class _ChartEngine {
15577
15733
  if (editHit) {
15578
15734
  e.preventDefault();
15579
15735
  const level = this.levels.find((l) => l.label === editHit.label);
15580
- if (level) {
15581
- this.selectedTradeLabel = level.label;
15582
- this.renderTradeLayer();
15583
- const { slPrice: _slE, tpPrice: _tpE } = this._resolveBracketPrices(level);
15584
- this._editOpenLabel = level.label;
15585
- this.emitter.emit("tradeLevelEditOpen", {
15586
- label: level.label,
15587
- type: level.type,
15588
- data: level.data,
15589
- price: level.price,
15590
- side: level.side,
15591
- stopLossPrice: _slE,
15592
- takeProfitPrice: _tpE,
15593
- isFullscreen: this.isFullscreen
15594
- });
15595
- }
15736
+ if (level) this._openTradeLevelEditForm(level);
15596
15737
  return;
15597
15738
  }
15598
15739
  }
@@ -15607,23 +15748,10 @@ var _ChartEngine = class _ChartEngine {
15607
15748
  if (this.hideLevelConfirmCancel && !isMobileDraggable) {
15608
15749
  const hasPending = this.pendingLevelDrags.has(level.label);
15609
15750
  if (this.selectedTradeLabel === level.label && !hasPending) {
15610
- this.selectedTradeLabel = null;
15611
- } else {
15612
- this.selectedTradeLabel = level.label;
15751
+ this._cancelEditAndDeselect(level.label);
15752
+ return;
15613
15753
  }
15614
- this.renderTradeLayer();
15615
- const { slPrice: _sl1, tpPrice: _tp1 } = this._resolveBracketPrices(level);
15616
- this._editOpenLabel = level.label;
15617
- this.emitter.emit("tradeLevelEditOpen", {
15618
- label: level.label,
15619
- type: level.type,
15620
- data: level.data,
15621
- price: level.price,
15622
- side: level.side,
15623
- stopLossPrice: _sl1,
15624
- takeProfitPrice: _tp1,
15625
- isFullscreen: this.isFullscreen
15626
- });
15754
+ this._openTradeLevelEditForm(level);
15627
15755
  return;
15628
15756
  }
15629
15757
  this._startTradeDragFromHit(dragHit, level, y);
@@ -15647,23 +15775,10 @@ var _ChartEngine = class _ChartEngine {
15647
15775
  if (level) {
15648
15776
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
15649
15777
  if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
15650
- this.selectedTradeLabel = null;
15651
- } else {
15652
- this.selectedTradeLabel = hoverHit.label;
15778
+ this._cancelEditAndDeselect(hoverHit.label);
15779
+ return;
15653
15780
  }
15654
- const { slPrice: _slH, tpPrice: _tpH } = this._resolveBracketPrices(level);
15655
- this._editOpenLabel = level.label;
15656
- this.renderTradeLayer();
15657
- this.emitter.emit("tradeLevelEditOpen", {
15658
- label: level.label,
15659
- type: level.type,
15660
- data: level.data,
15661
- price: level.price,
15662
- side: level.side,
15663
- stopLossPrice: _slH,
15664
- takeProfitPrice: _tpH,
15665
- isFullscreen: this.isFullscreen
15666
- });
15781
+ this._openTradeLevelEditForm(level);
15667
15782
  return;
15668
15783
  }
15669
15784
  }
@@ -15862,8 +15977,10 @@ var _ChartEngine = class _ChartEngine {
15862
15977
  return;
15863
15978
  }
15864
15979
  if (this.axisDrag) {
15980
+ const wasPriceAxis = this.axisDrag.axis === "price" || this.axisDrag.axis === "time";
15865
15981
  this.axisDrag = null;
15866
15982
  this.scheduleRender();
15983
+ if (wasPriceAxis) this.scheduleViewportPersist();
15867
15984
  return;
15868
15985
  }
15869
15986
  const wasAnchorTouch = this._drawTouchAnchorActive;
@@ -15948,22 +16065,7 @@ var _ChartEngine = class _ChartEngine {
15948
16065
  const editHit = hitTestTradeLevelEdit(x, y, this.tradeLevelEditAreas);
15949
16066
  if (editHit) {
15950
16067
  const level = this.levels.find((l) => l.label === editHit.label);
15951
- if (level) {
15952
- this.selectedTradeLabel = level.label;
15953
- this.renderTradeLayer();
15954
- const { slPrice: _sl2, tpPrice: _tp2 } = this._resolveBracketPrices(level);
15955
- this._editOpenLabel = level.label;
15956
- this.emitter.emit("tradeLevelEditOpen", {
15957
- label: level.label,
15958
- type: level.type,
15959
- data: level.data,
15960
- price: level.price,
15961
- side: level.side,
15962
- stopLossPrice: _sl2,
15963
- takeProfitPrice: _tp2,
15964
- isFullscreen: this.isFullscreen
15965
- });
15966
- }
16068
+ if (level) this._openTradeLevelEditForm(level);
15967
16069
  return;
15968
16070
  }
15969
16071
  }
@@ -15981,8 +16083,7 @@ var _ChartEngine = class _ChartEngine {
15981
16083
  const level = this.levels.find((l) => l.label === dragHit.label);
15982
16084
  if (level) {
15983
16085
  if (!(this.hideLevelConfirmCancel && !dragHit.isBracket)) {
15984
- this._startTradeDragFromHit(dragHit, level, y);
15985
- this.drawCanvas.style.cursor = "ns-resize";
16086
+ this._pendingTradeDrag = { dragHit, level, hitY: y, startX: x, startY: y };
15986
16087
  return;
15987
16088
  }
15988
16089
  }
@@ -15992,49 +16093,24 @@ var _ChartEngine = class _ChartEngine {
15992
16093
  if (hoverHit) {
15993
16094
  const hasPending = this.pendingLevelDrags.has(hoverHit.label);
15994
16095
  if (this.selectedTradeLabel === hoverHit.label && !hasPending) {
15995
- this.selectedTradeLabel = null;
16096
+ this._cancelEditAndDeselect(hoverHit.label);
16097
+ return;
16098
+ }
16099
+ const level = this.levels.find((l) => l.label === hoverHit.label);
16100
+ if (level) {
16101
+ this._openTradeLevelEditForm(level);
15996
16102
  } else {
15997
16103
  this.selectedTradeLabel = hoverHit.label;
15998
- }
15999
- this.renderTradeLayer();
16000
- if (this.hideLevelConfirmCancel) {
16001
- const level = this.levels.find((l) => l.label === hoverHit.label);
16002
- if (level) {
16003
- const { slPrice: _sl3, tpPrice: _tp3 } = this._resolveBracketPrices(level);
16004
- this._editOpenLabel = level.label;
16005
- this.emitter.emit("tradeLevelEditOpen", {
16006
- label: level.label,
16007
- type: level.type,
16008
- data: level.data,
16009
- price: level.price,
16010
- side: level.side,
16011
- stopLossPrice: _sl3,
16012
- takeProfitPrice: _tp3,
16013
- isFullscreen: this.isFullscreen
16014
- });
16015
- }
16104
+ this.renderTradeLayer();
16016
16105
  }
16017
16106
  return;
16018
16107
  } else if (this.selectedTradeLabel !== null) {
16019
16108
  if (!this.hideLevelConfirmCancel) {
16020
16109
  const lbl = this.selectedTradeLabel;
16021
16110
  const pending = this.pendingLevelDrags.get(lbl);
16022
- const isEditFormOpen = this._editOpenLabel === lbl;
16023
16111
  if (!pending || pending.length === 0) {
16024
- if (isEditFormOpen) {
16025
- const lvl = this.levels.find((l) => l.label === lbl);
16026
- this._editOpenLabel = null;
16027
- if (lvl) {
16028
- this.emitter.emit("tradeLevelEditCancelled", {
16029
- label: lbl,
16030
- type: lvl.type,
16031
- isFullscreen: this.isFullscreen
16032
- });
16033
- }
16034
- }
16035
- this.selectedTradeLabel = null;
16036
- this.renderTradeLayer();
16037
- } else if (pending.every((c) => c.isNew)) {
16112
+ this._cancelEditAndDeselect(lbl);
16113
+ } else {
16038
16114
  this.revertPendingChanges(lbl);
16039
16115
  }
16040
16116
  }
@@ -16089,10 +16165,9 @@ var _ChartEngine = class _ChartEngine {
16089
16165
  this.scheduleRender();
16090
16166
  return;
16091
16167
  }
16092
- const consumed = this.drawingManager.onMouseDown({
16093
- barIndex: rawBarIndex,
16094
- price
16095
- });
16168
+ const consumed = this.drawingManager.onMouseDown(
16169
+ this.makeDrawingPoint(rawBarIndex, price)
16170
+ );
16096
16171
  if (!consumed) {
16097
16172
  const handleHit = this.drawingManager.getHandleAtPoint(
16098
16173
  x,
@@ -16687,6 +16762,7 @@ var _ChartEngine = class _ChartEngine {
16687
16762
  this.tradePopover = new TradePopover(
16688
16763
  this.theme,
16689
16764
  0.01,
16765
+ this.symbol,
16690
16766
  (side, price, orderType) => {
16691
16767
  this.createDraftOrder(side, price, orderType);
16692
16768
  },
@@ -16790,6 +16866,12 @@ var _ChartEngine = class _ChartEngine {
16790
16866
  this.drawCanvas.style.cursor = "default";
16791
16867
  });
16792
16868
  this.drawingManager.onDrawingsChange = () => this.emitStateChange();
16869
+ this.drawingManager.onPointsMutated = (drawing) => {
16870
+ for (const pt of drawing.points) {
16871
+ const t = this.barIndexToTimestamp(pt.barIndex);
16872
+ if (t != null) pt.timestamp = t;
16873
+ }
16874
+ };
16793
16875
  this.drawingManager.onStartTextEdit = (drawing) => this.showTextInput(drawing);
16794
16876
  this.panHandler = new PanHandler(
16795
16877
  this.drawCanvas,
@@ -16799,16 +16881,19 @@ var _ChartEngine = class _ChartEngine {
16799
16881
  this.clearDuration();
16800
16882
  this.checkNeedMoreData();
16801
16883
  this.scheduleRender();
16884
+ this.scheduleViewportPersist();
16802
16885
  },
16803
16886
  () => this.dataStore.length,
16804
16887
  () => this.scheduleRender(),
16805
16888
  () => this.drawingManager.getActiveTool() !== null || this.drawingManager.isDraggingDrawing() || this.drawingManager.isResizingDrawing() || this.separatorDrag !== null || this.axisDrag !== null || (this.tradeDragHandler?.active ?? false) || this._mobileCrosshairActive,
16806
16889
  (deltaY) => {
16890
+ if (this.priceScaleFactor === 1) return;
16807
16891
  const { priceH } = this.getChartLayout();
16808
16892
  const currentRange = this._displayedPriceRange ?? this.getScaledPriceRange(this._targetViewport);
16809
16893
  const pricePerPixel = (currentRange.max - currentRange.min) / priceH;
16810
16894
  this.pricePanOffset += deltaY * pricePerPixel;
16811
16895
  this.scheduleRender();
16896
+ this.scheduleViewportPersist();
16812
16897
  },
16813
16898
  () => {
16814
16899
  this._isPanning = true;
@@ -16831,6 +16916,7 @@ var _ChartEngine = class _ChartEngine {
16831
16916
  this.clearDuration();
16832
16917
  this.checkNeedMoreData();
16833
16918
  this.scheduleRender();
16919
+ this.scheduleViewportPersist();
16834
16920
  },
16835
16921
  () => this.dataStore.length,
16836
16922
  () => this.scheduleRender(),
@@ -16851,6 +16937,7 @@ var _ChartEngine = class _ChartEngine {
16851
16937
  this.clearDuration();
16852
16938
  this.checkNeedMoreData();
16853
16939
  this.scheduleRender();
16940
+ this.scheduleViewportPersist();
16854
16941
  }
16855
16942
  );
16856
16943
  this.drawCanvas.addEventListener("mousemove", this.onMouseMove);
@@ -16888,6 +16975,15 @@ var _ChartEngine = class _ChartEngine {
16888
16975
  return true;
16889
16976
  }).map((s) => s.indicator);
16890
16977
  }
16978
+ /** Schedules a stateChange emission so viewport/priceScale changes made by
16979
+ * pan/zoom/price-scale drag reach persistence. Coalesces rapid calls. */
16980
+ scheduleViewportPersist() {
16981
+ if (this._viewportPersistTimer) clearTimeout(this._viewportPersistTimer);
16982
+ this._viewportPersistTimer = setTimeout(() => {
16983
+ this._viewportPersistTimer = null;
16984
+ this.emitStateChange();
16985
+ }, 500);
16986
+ }
16891
16987
  get isFullscreen() {
16892
16988
  return this._isCssFullscreen || !!document.fullscreenElement;
16893
16989
  }
@@ -16979,6 +17075,22 @@ var _ChartEngine = class _ChartEngine {
16979
17075
  }
16980
17076
  this.recomputeIndicators();
16981
17077
  this.reanchorDrawingsByTimestamp();
17078
+ if (this._pendingViewportRestore) {
17079
+ const pending = this._pendingViewportRestore;
17080
+ this._pendingViewportRestore = null;
17081
+ if (pending.priceScale) {
17082
+ this.priceScaleFactor = pending.priceScale.factor;
17083
+ this.pricePanOffset = pending.priceScale.panOffset;
17084
+ this._displayedPriceRange = null;
17085
+ this._lockedAutoFitBase = null;
17086
+ }
17087
+ if (pending.viewport) {
17088
+ this.applyViewportFromTimestamps(
17089
+ pending.viewport.startTime,
17090
+ pending.viewport.endTime
17091
+ );
17092
+ }
17093
+ }
16982
17094
  this.updateCountdownState();
16983
17095
  this.scheduleRender();
16984
17096
  return this;
@@ -16999,6 +17111,7 @@ var _ChartEngine = class _ChartEngine {
16999
17111
  };
17000
17112
  this._targetViewport = { ...this.viewport };
17001
17113
  this.drawingManager.shiftBarIndices(n);
17114
+ this.reanchorDrawingsByTimestamp();
17002
17115
  this.recomputeIndicators();
17003
17116
  this.scheduleRender();
17004
17117
  }
@@ -17522,15 +17635,44 @@ var _ChartEngine = class _ChartEngine {
17522
17635
  ...d,
17523
17636
  points: d.points.map((p) => ({
17524
17637
  ...p,
17525
- timestamp: this.dataStore.all[Math.floor(p.barIndex)]?.time
17638
+ // Prefer the point's own timestamp (captured at placement / drag /
17639
+ // resize); only derive from current bars as a fallback for legacy
17640
+ // points. Re-deriving on every export would otherwise corrupt the
17641
+ // anchor after a timeframe change has staled the cached barIndex.
17642
+ timestamp: p.timestamp ?? this.barIndexToTimestamp(p.barIndex)
17526
17643
  }))
17527
17644
  })),
17528
17645
  positionRenderStyle: this.positionRenderStyle,
17529
17646
  tradeDisplayFilter: this.tradeDisplayFilter,
17530
17647
  canvasColors: Object.keys(this.settingsCanvasColors).length > 0 ? { ...this.settingsCanvasColors } : void 0,
17531
- tfcEnabled: this.tfcActive
17648
+ tfcEnabled: this.tfcActive,
17649
+ viewport: this.getViewportTimestamps(),
17650
+ priceScale: this.priceScaleFactor !== 1 || this.pricePanOffset !== 0 ? { factor: this.priceScaleFactor, panOffset: this.pricePanOffset } : void 0
17532
17651
  };
17533
17652
  }
17653
+ /**
17654
+ * Captures the current viewport as a {startTime, endTime} timestamp pair,
17655
+ * preserving sub-bar fractional position. Returns undefined only when no
17656
+ * bars are loaded (viewport has no meaningful time anchor).
17657
+ */
17658
+ getViewportTimestamps() {
17659
+ if (this.dataStore.all.length === 0) return void 0;
17660
+ const startTime = this.barIndexToTimestamp(this.viewport.startIndex);
17661
+ const endTime = this.barIndexToTimestamp(this.viewport.endIndex);
17662
+ if (startTime == null || endTime == null) return void 0;
17663
+ return { startTime, endTime };
17664
+ }
17665
+ /**
17666
+ * Restores the viewport from a timestamp pair, converting each edge back
17667
+ * into a fractional bar index relative to the current dataStore. Callers
17668
+ * must ensure bars are loaded before invoking.
17669
+ */
17670
+ applyViewportFromTimestamps(startTime, endTime) {
17671
+ const startIndex = this.timestampToBarIndex(startTime);
17672
+ const endIndex = this.timestampToBarIndex(endTime);
17673
+ this.viewport = { ...this.viewport, startIndex, endIndex };
17674
+ this._targetViewport = { ...this.viewport };
17675
+ }
17534
17676
  /**
17535
17677
  * Restores chart configuration from a previously captured `ChartState`.
17536
17678
  * Call this after construction to hydrate a chart from persisted state.
@@ -17577,18 +17719,11 @@ var _ChartEngine = class _ChartEngine {
17577
17719
  }
17578
17720
  }
17579
17721
  }
17580
- const bars = this.dataStore.all;
17581
17722
  const resolvedDrawings = state.drawings.map((d) => ({
17582
17723
  ...d,
17583
17724
  points: d.points.map((p) => {
17584
- if (!p.timestamp || bars.length === 0) return p;
17585
- let lo = 0, hi = bars.length - 1;
17586
- while (lo < hi) {
17587
- const mid = lo + hi >> 1;
17588
- if ((bars[mid]?.time ?? 0) < p.timestamp) lo = mid + 1;
17589
- else hi = mid;
17590
- }
17591
- return { ...p, barIndex: lo + (p.barIndex - Math.floor(p.barIndex)) };
17725
+ if (p.timestamp == null || this.dataStore.all.length === 0) return p;
17726
+ return { ...p, barIndex: this.timestampToBarIndex(p.timestamp) };
17592
17727
  })
17593
17728
  }));
17594
17729
  this.drawingManager.loadDrawings(resolvedDrawings);
@@ -17606,6 +17741,25 @@ var _ChartEngine = class _ChartEngine {
17606
17741
  this.tfcActive = state.tfcEnabled;
17607
17742
  this.topBar?.setTfcActive(state.tfcEnabled);
17608
17743
  }
17744
+ if (state.priceScale) {
17745
+ this.priceScaleFactor = state.priceScale.factor;
17746
+ this.pricePanOffset = state.priceScale.panOffset;
17747
+ this._displayedPriceRange = null;
17748
+ this._lockedAutoFitBase = null;
17749
+ }
17750
+ if (state.viewport) {
17751
+ if (this.dataStore.all.length > 0) {
17752
+ this.applyViewportFromTimestamps(
17753
+ state.viewport.startTime,
17754
+ state.viewport.endTime
17755
+ );
17756
+ } else {
17757
+ this._pendingViewportRestore = {
17758
+ viewport: state.viewport,
17759
+ priceScale: state.priceScale
17760
+ };
17761
+ }
17762
+ }
17609
17763
  } finally {
17610
17764
  this.isRestoringState = false;
17611
17765
  }
@@ -17614,23 +17768,120 @@ var _ChartEngine = class _ChartEngine {
17614
17768
  return this;
17615
17769
  }
17616
17770
  /**
17617
- * Re-anchors all loaded drawings to the current dataset using their stored timestamps.
17618
- * Called after loadData() to fix drawings restored via setState() before data was
17619
- * available (bars.length === 0 at restore time causes the raw barIndex fallback).
17771
+ * Converts a fractional barIndex into an absolute unix-ms timestamp by
17772
+ * interpolating across the containing bar. Extrapolates linearly for indexes
17773
+ * outside [0, bars.length-1] using the adjacent bar's width.
17774
+ *
17775
+ * This is the inverse of `timestampToBarIndex` and the authoritative way to
17776
+ * capture "where in time" a drawing point sits at placement, drag, or resize.
17777
+ * Returns undefined only when no bars are loaded.
17778
+ */
17779
+ barIndexToTimestamp(idx) {
17780
+ const bars = this.dataStore.all;
17781
+ const n = bars.length;
17782
+ if (n === 0) return void 0;
17783
+ const first = bars[0];
17784
+ if (!first) return void 0;
17785
+ if (n === 1) return first.time;
17786
+ const floorIdx = Math.floor(idx);
17787
+ const frac = idx - floorIdx;
17788
+ if (floorIdx < 0) {
17789
+ const second = bars[1];
17790
+ if (!second) return first.time;
17791
+ return first.time + idx * (second.time - first.time);
17792
+ }
17793
+ if (floorIdx >= n - 1) {
17794
+ const last = bars[n - 1];
17795
+ const prev = bars[n - 2];
17796
+ if (!last || !prev) return first.time;
17797
+ return last.time + (idx - (n - 1)) * (last.time - prev.time);
17798
+ }
17799
+ const lo = bars[floorIdx];
17800
+ const hi = bars[floorIdx + 1];
17801
+ if (!lo || !hi) return first.time;
17802
+ return lo.time + frac * (hi.time - lo.time);
17803
+ }
17804
+ /**
17805
+ * Converts a unix-ms timestamp into a fractional barIndex using floor-bar
17806
+ * containment: the result is `floorBar + (ts - floorBar.time) / (nextBar.time - floorBar.time)`.
17807
+ * Extrapolates linearly for timestamps outside the loaded range using the edge
17808
+ * bar width, so drawings outside the visible data still render at the correct
17809
+ * relative offset rather than snapping to an edge.
17810
+ *
17811
+ * Matches TradingView's floor-bar semantics and preserves sub-bar time
17812
+ * precision across timeframe changes.
17813
+ */
17814
+ timestampToBarIndex(ts) {
17815
+ const bars = this.dataStore.all;
17816
+ const n = bars.length;
17817
+ if (n === 0) return 0;
17818
+ const first = bars[0];
17819
+ if (!first) return 0;
17820
+ if (n === 1) return 0;
17821
+ if (ts <= first.time) {
17822
+ const second = bars[1];
17823
+ if (!second) return 0;
17824
+ const barMs2 = second.time - first.time;
17825
+ return barMs2 > 0 ? (ts - first.time) / barMs2 : 0;
17826
+ }
17827
+ const lastIdx = n - 1;
17828
+ const lastBar = bars[lastIdx];
17829
+ const prevBar = bars[lastIdx - 1];
17830
+ if (!lastBar || !prevBar) return 0;
17831
+ if (ts >= lastBar.time) {
17832
+ const barMs2 = lastBar.time - prevBar.time;
17833
+ return lastIdx + (barMs2 > 0 ? (ts - lastBar.time) / barMs2 : 0);
17834
+ }
17835
+ let lo = 0, hi = lastIdx;
17836
+ while (lo < hi) {
17837
+ const mid = lo + hi + 1 >> 1;
17838
+ if ((bars[mid]?.time ?? 0) <= ts) lo = mid;
17839
+ else hi = mid - 1;
17840
+ }
17841
+ const loBar = bars[lo];
17842
+ const hiBar = bars[lo + 1];
17843
+ if (!loBar || !hiBar) return lo;
17844
+ const barMs = hiBar.time - loBar.time;
17845
+ return lo + (barMs > 0 ? (ts - loBar.time) / barMs : 0);
17846
+ }
17847
+ /**
17848
+ * Re-anchors all loaded drawings to the current dataset. For points with a
17849
+ * stored timestamp, barIndex is recomputed via `timestampToBarIndex`; for
17850
+ * points missing a timestamp (legacy saves or freshly placed points whose
17851
+ * timestamp capture was skipped), the timestamp is backfilled from the
17852
+ * current barIndex so subsequent data reloads remain accurate.
17853
+ *
17854
+ * Called after loadData(), prependData(), and whenever the underlying bars
17855
+ * change such that cached barIndex values would otherwise drift.
17620
17856
  */
17621
17857
  reanchorDrawingsByTimestamp() {
17622
17858
  const bars = this.dataStore.all;
17623
17859
  if (bars.length === 0) return;
17624
17860
  for (const drawing of this.drawingManager.getDrawings()) {
17625
17861
  for (const pt of drawing.points) {
17626
- if (!pt.timestamp) continue;
17627
- let lo = 0, hi = bars.length - 1;
17628
- while (lo < hi) {
17629
- const mid = lo + hi >> 1;
17630
- if ((bars[mid]?.time ?? 0) < pt.timestamp) lo = mid + 1;
17631
- else hi = mid;
17862
+ if (pt.timestamp == null) {
17863
+ const t = this.barIndexToTimestamp(pt.barIndex);
17864
+ if (t != null) pt.timestamp = t;
17865
+ continue;
17632
17866
  }
17633
- pt.barIndex = lo + (pt.barIndex - Math.floor(pt.barIndex));
17867
+ pt.barIndex = this.timestampToBarIndex(pt.timestamp);
17868
+ }
17869
+ }
17870
+ }
17871
+ /**
17872
+ * Captures a timestamp on any drawing point that is missing one, using the
17873
+ * current bars. Call this BEFORE mutating `dataStore.all` (insert/splice)
17874
+ * so that points still pointing at a stale barIndex get frozen to the
17875
+ * correct time before the mutation invalidates the index. Safe no-op for
17876
+ * points that already have a timestamp.
17877
+ */
17878
+ backfillDrawingTimestamps() {
17879
+ if (this.dataStore.all.length === 0) return;
17880
+ for (const drawing of this.drawingManager.getDrawings()) {
17881
+ for (const pt of drawing.points) {
17882
+ if (pt.timestamp != null) continue;
17883
+ const t = this.barIndexToTimestamp(pt.barIndex);
17884
+ if (t != null) pt.timestamp = t;
17634
17885
  }
17635
17886
  }
17636
17887
  }
@@ -17660,6 +17911,7 @@ var _ChartEngine = class _ChartEngine {
17660
17911
  }
17661
17912
  setSymbol(symbol) {
17662
17913
  this.symbol = symbol;
17914
+ this.tradePopover?.setSymbol(symbol);
17663
17915
  if (this.siSymbolSpan) {
17664
17916
  this.siSymbolSpan.textContent = symbol;
17665
17917
  this.siSymbolSpan.style.display = symbol ? "inline" : "none";
@@ -18667,7 +18919,7 @@ var _ChartEngine = class _ChartEngine {
18667
18919
  return;
18668
18920
  }
18669
18921
  const { chartW, priceH } = this.getChartLayout();
18670
- const priceRange = this.getScaledPriceRange(viewport);
18922
+ const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(viewport);
18671
18923
  const bracketOrders = this.levels.filter(
18672
18924
  (l) => l.type === "pending" && l.ToClose != null
18673
18925
  );
@@ -18732,7 +18984,12 @@ var _ChartEngine = class _ChartEngine {
18732
18984
  this.tradeDragHandler?.dragLabel ?? null,
18733
18985
  this.tradeDragNewPrice,
18734
18986
  this.selectedTradeLabel,
18735
- new Set(this.pendingLevelDrags.keys()),
18987
+ // Include _editOpenLabel so confirm/cancel buttons appear while the edit form is open
18988
+ // even before any change is staged — e.g. editing a position with no SL/TP.
18989
+ /* @__PURE__ */ new Set([
18990
+ ...this.pendingLevelDrags.keys(),
18991
+ ...this._editOpenLabel !== null ? [this._editOpenLabel] : []
18992
+ ]),
18736
18993
  this.positionRenderStyle,
18737
18994
  this.hideLevelConfirmCancel,
18738
18995
  this.draftOrderLabel,
@@ -18935,6 +19192,11 @@ var _ChartEngine = class _ChartEngine {
18935
19192
  }
18936
19193
  }
18937
19194
  }
19195
+ if (result.newPrice === oldPrice) {
19196
+ this.tradeDragNewPrice = null;
19197
+ this.renderTradeLayer();
19198
+ return;
19199
+ }
18938
19200
  const field = result.bracketType ?? "main";
18939
19201
  const isNew = this._isBracketNew;
18940
19202
  this._isBracketNew = false;
@@ -19040,7 +19302,22 @@ var _ChartEngine = class _ChartEngine {
19040
19302
  }
19041
19303
  applyPendingChanges(label) {
19042
19304
  const changes = this.pendingLevelDrags.get(label);
19043
- if (!changes) return;
19305
+ if (!changes) {
19306
+ if (this._editOpenLabel === label) {
19307
+ const level2 = this.levels.find((l) => l.label === label);
19308
+ this._editOpenLabel = null;
19309
+ this.selectedTradeLabel = null;
19310
+ this.renderTradeLayer();
19311
+ if (level2) {
19312
+ this.emitter.emit("tradeLevelConfirmed", {
19313
+ label,
19314
+ type: level2.type,
19315
+ isFullscreen: this.isFullscreen
19316
+ });
19317
+ }
19318
+ }
19319
+ return;
19320
+ }
19044
19321
  const level = this.levels.find((l) => l.label === label);
19045
19322
  if (!level) return;
19046
19323
  if (this._editOpenLabel === label) this._editOpenLabel = null;
@@ -19119,6 +19396,10 @@ var _ChartEngine = class _ChartEngine {
19119
19396
  if (!label && this.draftOrderLabel) {
19120
19397
  this.removeDraftOrder();
19121
19398
  }
19399
+ if (label && !this.pendingLevelDrags.has(label) && this._editOpenLabel === label) {
19400
+ this._cancelEditAndDeselect(label);
19401
+ return;
19402
+ }
19122
19403
  const toRevert = label ? this.pendingLevelDrags.has(label) ? [[label, this.pendingLevelDrags.get(label) ?? []]] : [] : [...this.pendingLevelDrags.entries()];
19123
19404
  for (const [lbl] of toRevert) {
19124
19405
  this.undoIsNewBracketMutations(lbl);
@@ -19691,6 +19972,7 @@ var _ChartEngine = class _ChartEngine {
19691
19972
  this.leftBar?.setMobileMode(isMobile);
19692
19973
  this.topBar?.setMobileLayout(isMobile);
19693
19974
  this.tradeButton?.setMobileMode(isMobile);
19975
+ this.tradePopover?.setCompact(isMobile);
19694
19976
  if (this.siEl) {
19695
19977
  this.siEl.style.flexDirection = isMobile ? "column" : "row";
19696
19978
  this.siEl.style.alignItems = isMobile ? "flex-start" : "center";
@@ -19781,10 +20063,21 @@ var _ChartEngine = class _ChartEngine {
19781
20063
  const y = touch.clientY - rect.top;
19782
20064
  const { chartW, priceH } = this.getChartLayout();
19783
20065
  const priceRange = this._displayedPriceRange ?? this.getScaledPriceRange(this.viewport);
19784
- return {
19785
- barIndex: this.scaleManager.pixelToBarIndex(x, this.viewport, chartW),
19786
- price: this.scaleManager.pixelToPrice(y, priceRange, priceH)
19787
- };
20066
+ return this.makeDrawingPoint(
20067
+ this.scaleManager.pixelToBarIndex(x, this.viewport, chartW),
20068
+ this.scaleManager.pixelToPrice(y, priceRange, priceH)
20069
+ );
20070
+ }
20071
+ /**
20072
+ * Builds a fully-populated DrawingPoint from raw float coordinates. Captures
20073
+ * the absolute timestamp at this fractional barIndex so the anchor survives
20074
+ * timeframe changes, data reloads, and serialization round-trips.
20075
+ */
20076
+ makeDrawingPoint(barIndex, price) {
20077
+ const point = { barIndex, price };
20078
+ const t = this.barIndexToTimestamp(barIndex);
20079
+ if (t != null) point.timestamp = t;
20080
+ return point;
19788
20081
  }
19789
20082
  /** Shared close-button handler used by both mouse click and touch tap. */
19790
20083
  _handleTradeLevelClose(closedLevel) {
@@ -19894,6 +20187,49 @@ var _ChartEngine = class _ChartEngine {
19894
20187
  }
19895
20188
  this.scheduleRender();
19896
20189
  }
20190
+ /**
20191
+ * Closes the edit form, clears selection, and emits `tradeLevelEditCancelled` when
20192
+ * the edit form was actually open for this label. Shared between click-outside,
20193
+ * repeat-click toggle, and the ✗ button so all exit paths produce the same payload.
20194
+ */
20195
+ _cancelEditAndDeselect(label) {
20196
+ const wasEditFormOpen = this._editOpenLabel === label;
20197
+ const level = this.levels.find((l) => l.label === label);
20198
+ this._editOpenLabel = null;
20199
+ this.selectedTradeLabel = null;
20200
+ this.draftBracketPnl = {};
20201
+ this.renderTradeLayer();
20202
+ if (wasEditFormOpen && level) {
20203
+ this.emitter.emit("tradeLevelEditCancelled", {
20204
+ label,
20205
+ type: level.type,
20206
+ isFullscreen: this.isFullscreen
20207
+ });
20208
+ }
20209
+ }
20210
+ /**
20211
+ * Opens the external edit form for a trade level by emitting `tradeLevelEditOpen`.
20212
+ * Shared between pencil click, bare box click, and drag start so every entry
20213
+ * point fires the same payload. No-op when the form is already open for this
20214
+ * label, so repeat opens (e.g. pencil click followed by drag) don't spam the event.
20215
+ */
20216
+ _openTradeLevelEditForm(level) {
20217
+ if (this._editOpenLabel === level.label) return;
20218
+ this.selectedTradeLabel = level.label;
20219
+ this._editOpenLabel = level.label;
20220
+ const { slPrice, tpPrice } = this._resolveBracketPrices(level);
20221
+ this.renderTradeLayer();
20222
+ this.emitter.emit("tradeLevelEditOpen", {
20223
+ label: level.label,
20224
+ type: level.type,
20225
+ data: level.data,
20226
+ price: level.price,
20227
+ side: level.side,
20228
+ stopLossPrice: slPrice,
20229
+ takeProfitPrice: tpPrice,
20230
+ isFullscreen: this.isFullscreen
20231
+ });
20232
+ }
19897
20233
  /** Shared helper: initiate a trade level main-line drag from a hit area. */
19898
20234
  _startTradeDragFromHit(dragHit, level, hitY) {
19899
20235
  if (!this.tradeDragHandler) return;
@@ -19969,6 +20305,7 @@ var _ChartEngine = class _ChartEngine {
19969
20305
  );
19970
20306
  }
19971
20307
  }
20308
+ this._openTradeLevelEditForm(level);
19972
20309
  }
19973
20310
  /** Shared helper: initiate a +SL/+TP add-bracket drag from a hit area. */
19974
20311
  _startAddBracketFromHit(addHit, hitY) {
@@ -20082,6 +20419,7 @@ var _ChartEngine = class _ChartEngine {
20082
20419
  this.dataLoader({ start: new Date(fromTime), end: new Date(toTime), interval }).then((rawBars) => {
20083
20420
  const bars = this.maybeAggregate(rawBars);
20084
20421
  if (bars.length === 0) return;
20422
+ this.backfillDrawingTimestamps();
20085
20423
  const all = this.dataStore.all;
20086
20424
  const liveBar = all[all.length - 1];
20087
20425
  let insertCount = 0;
@@ -20102,6 +20440,7 @@ var _ChartEngine = class _ChartEngine {
20102
20440
  startIndex: this.viewport.startIndex + insertCount,
20103
20441
  endIndex: this.viewport.endIndex + insertCount
20104
20442
  };
20443
+ this.reanchorDrawingsByTimestamp();
20105
20444
  this.recomputeIndicators();
20106
20445
  this.scheduleRender();
20107
20446
  }
@@ -20296,6 +20635,7 @@ var _ChartEngine = class _ChartEngine {
20296
20635
  }
20297
20636
  };
20298
20637
  _ChartEngine.DRAW_GESTURE_THRESHOLD = 4;
20638
+ _ChartEngine.TRADE_DRAG_PIXEL_THRESHOLD = 3;
20299
20639
  var ChartEngine = _ChartEngine;
20300
20640
 
20301
20641
  // src/data/WebSocketAdapter.ts