acttrader-charts 1.0.19 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -257,6 +257,8 @@ interface TradeLabels {
257
257
  stop: string;
258
258
  /** Market order type label. */
259
259
  market: string;
260
+ /** Separator word between order descriptor and price (e.g. "at"). */
261
+ at: string;
260
262
  /** Quantity input label in the draft-order input. */
261
263
  qty: string;
262
264
  /** Tooltip on the floating trade button (⊕). */
@@ -397,6 +399,10 @@ interface IndicatorOverlayColors {
397
399
  }
398
400
  /** Colors for trade/position horizontal level lines. */
399
401
  interface TradeLevelColors {
402
+ /** Solid accent color for buy-side UI elements (popover text, button, icon). */
403
+ buySide: string;
404
+ /** Solid accent color for sell-side UI elements (popover text, button, icon). */
405
+ sellSide: string;
400
406
  /** Line + box border color for open executed trades. */
401
407
  tradeLine: string;
402
408
  /** Line + box border color for open positions. */
@@ -257,6 +257,8 @@ interface TradeLabels {
257
257
  stop: string;
258
258
  /** Market order type label. */
259
259
  market: string;
260
+ /** Separator word between order descriptor and price (e.g. "at"). */
261
+ at: string;
260
262
  /** Quantity input label in the draft-order input. */
261
263
  qty: string;
262
264
  /** Tooltip on the floating trade button (⊕). */
@@ -397,6 +399,10 @@ interface IndicatorOverlayColors {
397
399
  }
398
400
  /** Colors for trade/position horizontal level lines. */
399
401
  interface TradeLevelColors {
402
+ /** Solid accent color for buy-side UI elements (popover text, button, icon). */
403
+ buySide: string;
404
+ /** Solid accent color for sell-side UI elements (popover text, button, icon). */
405
+ sellSide: string;
400
406
  /** Line + box border color for open executed trades. */
401
407
  tradeLine: string;
402
408
  /** Line + box border color for open positions. */
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(
@@ -13189,8 +13246,8 @@ var TradeButton = class {
13189
13246
  this.el.style.opacity = "1";
13190
13247
  Object.assign(this.el.style, {
13191
13248
  background: theme.tooltip.background,
13192
- color: "#26a69a",
13193
- border: "1px solid #26a69a"
13249
+ color: theme.tradeLevels.buySide,
13250
+ border: `1px solid ${theme.tradeLevels.buySide}`
13194
13251
  });
13195
13252
  });
13196
13253
  this.el.addEventListener("mouseleave", (e) => {
@@ -13253,11 +13310,12 @@ var TradeButton = class {
13253
13310
 
13254
13311
  // src/ui/TradePopover.ts
13255
13312
  var TradePopover = class {
13256
- constructor(theme, lots, onDraftCreate, labels = DEFAULT_LABELS.trade, onVisibilityChange) {
13313
+ constructor(theme, lots, symbol, onDraftCreate, labels = DEFAULT_LABELS.trade, onVisibilityChange) {
13257
13314
  this._price = 0;
13258
13315
  this._pricePrecision = 2;
13259
13316
  this._buyOrderType = "limit";
13260
13317
  this._sellOrderType = "limit";
13318
+ this._compact = false;
13261
13319
  this.onDocMouseDown = (e) => {
13262
13320
  if (this.isVisible && !this.el.contains(e.target)) {
13263
13321
  this.hide();
@@ -13265,6 +13323,7 @@ var TradePopover = class {
13265
13323
  };
13266
13324
  this.theme = theme;
13267
13325
  this.lots = lots;
13326
+ this.symbol = symbol;
13268
13327
  this.onDraftCreate = onDraftCreate;
13269
13328
  this.labels = labels;
13270
13329
  this.onVisibilityChange = onVisibilityChange;
@@ -13273,7 +13332,6 @@ var TradePopover = class {
13273
13332
  position: "absolute",
13274
13333
  display: "none",
13275
13334
  flexDirection: "column",
13276
- gap: "1px",
13277
13335
  background: theme.tooltip.background,
13278
13336
  border: `1px solid ${theme.axisBorder}`,
13279
13337
  borderRadius: "5px",
@@ -13282,7 +13340,7 @@ var TradePopover = class {
13282
13340
  boxShadow: "0 4px 16px rgba(0,0,0,0.25)",
13283
13341
  fontFamily: "'Inter', system-ui, sans-serif",
13284
13342
  fontSize: "12px",
13285
- minWidth: "140px",
13343
+ width: "max-content",
13286
13344
  pointerEvents: "auto"
13287
13345
  });
13288
13346
  document.addEventListener("mousedown", this.onDocMouseDown);
@@ -13296,10 +13354,8 @@ var TradePopover = class {
13296
13354
  this._sellOrderType = sellOrderType;
13297
13355
  this.rebuild();
13298
13356
  this.el.style.display = "flex";
13299
- const popW = 145;
13300
13357
  this.el.style.right = `${buttonRight}px`;
13301
13358
  this.el.style.top = `${buttonY - 16}px`;
13302
- this.el.style.width = `${popW}px`;
13303
13359
  if (!wasVisible) this.onVisibilityChange?.();
13304
13360
  }
13305
13361
  hide() {
@@ -13313,6 +13369,12 @@ var TradePopover = class {
13313
13369
  setLots(lots) {
13314
13370
  this.lots = lots;
13315
13371
  }
13372
+ setSymbol(symbol) {
13373
+ this.symbol = symbol;
13374
+ }
13375
+ setCompact(compact) {
13376
+ this._compact = compact;
13377
+ }
13316
13378
  setTheme(theme) {
13317
13379
  this.theme = theme;
13318
13380
  Object.assign(this.el.style, {
@@ -13326,31 +13388,46 @@ var TradePopover = class {
13326
13388
  // ── Private ───────────────────────────────────────────────────────────────
13327
13389
  rebuild() {
13328
13390
  this.el.innerHTML = "";
13391
+ const divider = document.createElement("div");
13392
+ Object.assign(divider.style, {
13393
+ height: "1px",
13394
+ background: this.theme.axisBorder,
13395
+ margin: "0 8px"
13396
+ });
13329
13397
  this.el.append(
13330
- this.makeRow(this.labels.buy, "#26a69a", "buy", this._buyOrderType),
13331
- this.makeRow(this.labels.sell, "#ef5350", "sell", this._sellOrderType)
13398
+ this.makeRow(this.theme.tradeLevels.sellSide, "sell", this._sellOrderType),
13399
+ divider,
13400
+ this.makeRow(this.theme.tradeLevels.buySide, "buy", this._buyOrderType)
13332
13401
  );
13333
13402
  }
13334
- makeRow(label, color, side, orderType) {
13403
+ makeRow(color, side, orderType) {
13335
13404
  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
13405
  Object.assign(btn.style, {
13339
- display: "block",
13406
+ display: "flex",
13407
+ alignItems: "center",
13408
+ gap: "8px",
13340
13409
  width: "100%",
13341
13410
  background: "transparent",
13342
- color,
13343
13411
  border: "none",
13344
13412
  borderRadius: "3px",
13345
- padding: "7px 10px",
13346
- textAlign: "left",
13413
+ padding: this._compact ? "5px 8px" : "8px 10px",
13347
13414
  cursor: "pointer",
13348
- fontSize: "12px",
13349
13415
  fontFamily: "'Inter', system-ui, sans-serif",
13350
- fontWeight: "600"
13416
+ fontSize: this._compact ? "11px" : "12px"
13351
13417
  });
13418
+ btn.appendChild(this.makeChevron(color, side));
13419
+ const sideLabel = side === "sell" ? this.labels.sell : this.labels.buy;
13420
+ const orderTypeLabel = orderType === "limit" ? this.labels.limit : this.labels.stop;
13421
+ const priceStr = this._price.toFixed(this._pricePrecision);
13422
+ const colored = document.createElement("span");
13423
+ colored.textContent = `${sideLabel} ${orderTypeLabel}`;
13424
+ Object.assign(colored.style, { color, fontWeight: "600" });
13425
+ const plain = document.createElement("span");
13426
+ plain.textContent = ` ${this.lots} ${this.symbol} ${this.labels.at} ${priceStr}`;
13427
+ Object.assign(plain.style, { color: "#ffffff", fontWeight: "400" });
13428
+ btn.append(colored, plain);
13352
13429
  btn.addEventListener("mouseenter", () => {
13353
- btn.style.background = color + "22";
13430
+ btn.style.background = "rgba(255,255,255,0.06)";
13354
13431
  });
13355
13432
  btn.addEventListener("mouseleave", () => {
13356
13433
  btn.style.background = "transparent";
@@ -13362,6 +13439,23 @@ var TradePopover = class {
13362
13439
  });
13363
13440
  return btn;
13364
13441
  }
13442
+ makeChevron(color, side) {
13443
+ const ns = "http://www.w3.org/2000/svg";
13444
+ const svg = document.createElementNS(ns, "svg");
13445
+ svg.setAttribute("width", "10");
13446
+ svg.setAttribute("height", "8");
13447
+ svg.setAttribute("viewBox", "0 0 10 8");
13448
+ svg.style.flexShrink = "0";
13449
+ const path = document.createElementNS(ns, "path");
13450
+ path.setAttribute("d", side === "sell" ? "M1 1 L5 7 L9 1" : "M1 7 L5 1 L9 7");
13451
+ path.setAttribute("stroke", color);
13452
+ path.setAttribute("fill", "none");
13453
+ path.setAttribute("stroke-width", "1.5");
13454
+ path.setAttribute("stroke-linecap", "round");
13455
+ path.setAttribute("stroke-linejoin", "round");
13456
+ svg.appendChild(path);
13457
+ return svg;
13458
+ }
13365
13459
  };
13366
13460
 
13367
13461
  // src/ui/DraftQtyFlyout.ts
@@ -19195,6 +19289,7 @@ var _ChartEngine = class _ChartEngine {
19195
19289
  this.tradePopover = new TradePopover(
19196
19290
  this.theme,
19197
19291
  0.01,
19292
+ this.symbol,
19198
19293
  (side, price, orderType) => {
19199
19294
  this.createDraftOrder(side, price, orderType);
19200
19295
  },
@@ -19312,6 +19407,7 @@ var _ChartEngine = class _ChartEngine {
19312
19407
  () => this.scheduleRender(),
19313
19408
  () => this.drawingManager.getActiveTool() !== null || this.drawingManager.isDraggingDrawing() || this.drawingManager.isResizingDrawing() || this.separatorDrag !== null || this.axisDrag !== null || (this.tradeDragHandler?.active ?? false) || this._mobileCrosshairActive,
19314
19409
  (deltaY) => {
19410
+ if (this.priceScaleFactor === 1) return;
19315
19411
  const { priceH } = this.getChartLayout();
19316
19412
  const currentRange = this._displayedPriceRange ?? this.getScaledPriceRange(this._targetViewport);
19317
19413
  const pricePerPixel = (currentRange.max - currentRange.min) / priceH;
@@ -20168,6 +20264,7 @@ var _ChartEngine = class _ChartEngine {
20168
20264
  }
20169
20265
  setSymbol(symbol) {
20170
20266
  this.symbol = symbol;
20267
+ this.tradePopover?.setSymbol(symbol);
20171
20268
  if (this.siSymbolSpan) {
20172
20269
  this.siSymbolSpan.textContent = symbol;
20173
20270
  this.siSymbolSpan.style.display = symbol ? "inline" : "none";
@@ -22199,6 +22296,7 @@ var _ChartEngine = class _ChartEngine {
22199
22296
  this.leftBar?.setMobileMode(isMobile);
22200
22297
  this.topBar?.setMobileLayout(isMobile);
22201
22298
  this.tradeButton?.setMobileMode(isMobile);
22299
+ this.tradePopover?.setCompact(isMobile);
22202
22300
  if (this.siEl) {
22203
22301
  this.siEl.style.flexDirection = isMobile ? "column" : "row";
22204
22302
  this.siEl.style.alignItems = isMobile ? "flex-start" : "center";