acttrader-charts 1.3.0-beta.17 → 1.3.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -395,7 +395,9 @@ Alert | Pending | SL | TP | TSL | ×
395
395
  row creates the draft order exactly as before and closes both.
396
396
  - **SL / TP / TSL** emit `bulkLevelsRequested` with `{ kind, price }` and close the
397
397
  bar — the host opens its Bulk Levels dialog pre-focused on that kind. The chart
398
- never calls a broker.
398
+ never calls a broker. The chips grey out, with a tooltip saying why, when the
399
+ symbol has no open positions or has buys and sells open at once — one stop or
400
+ target price cannot sit on the right side of the market for both directions.
399
401
  - **×**, a click elsewhere, Escape, TFC off and `dismissAllUI()` all close it.
400
402
 
401
403
  Chip labels and the × tooltip live under `labels.trade` (`actionAlert`, `actionPending`,
@@ -424,7 +426,8 @@ the total without opening the positions panel:
424
426
  - **◎** (the same target glyph as the Bulk Levels dialog header) emits
425
427
  `bulkLevelsRequested` with `source: 'pill'` and a `null` price, so your Bulk Levels
426
428
  dialog opens pre-scoped to every open position on the symbol. Tooltip:
427
- "Set SL · TP · TSL on all N {symbol} positions".
429
+ "Set SL · TP · TSL on all N {symbol} positions". Greyed out while buys and sells are
430
+ both open ("Bulk levels need all N {symbol} positions in one direction").
428
431
  - **✕** (amber) emits `pnlPillCloseAll` with every counted position. Your app
429
432
  confirms and closes them; the chart never calls a broker.
430
433
 
@@ -825,10 +828,12 @@ new ChartEngine({
825
828
  | `trade` | `actionAlert` … `actionTsl` | `'Alert'`, `'Pending'`, `'SL'`, `'TP'`, `'TSL'` | Trade action bar chips (`features.tradeActionBar`) |
826
829
  | `trade` | `actionCloseTitle` | `'Close'` | Trade action bar × tooltip |
827
830
  | `trade` | `directionAbove`, `directionBelow` | `'Above'`, `'Below'` | Pending sub-menu header — side of the market the clicked price is on |
831
+ | `trade` | `actionBulkNoPositionsTitle`, `actionBulkMixedTitle` | `'No open {symbol} positions'`, `'Bulk levels need all {symbol} positions in one direction'` | SL / TP / TSL chips greyed out — why |
828
832
  | `pnlPill` | `positionsAbbrev` | `'POS'` | On-chart P/L pill — word after the count (`3 POS`), shown uppercase |
829
833
  | `pnlPill` | `showPositionsTitle` | `'Show positions on chart · drag handle to move · double-click to reset'` | Pill tooltip while collapsed — a body click fans the positions out |
830
834
  | `pnlPill` | `hidePositionsTitle` | `'Hide positions on chart · drag handle to move · double-click to reset'` | Pill tooltip while fanned out |
831
835
  | `pnlPill` | `bulkLevelsTitle` | `'Set SL · TP · TSL on all {count} {symbol} positions'` | ◎ tooltip — opens the host's Bulk Levels dialog |
836
+ | `pnlPill` | `bulkLevelsMixedTitle` | `'Bulk levels need all {count} {symbol} positions in one direction'` | ◎ tooltip while greyed out for mixed sides |
832
837
  | `pnlPill` | `closeAllTitle` | `'Close all {symbol} positions'` | Amber ✕ tooltip; `{symbol}` and `{count}` are filled in |
833
838
  | `dialogs.settings` | `title` | `'Chart Settings'` | Dialog title |
834
839
  | `dialogs.settings` | `tfcSection` | `'Trade from Charts'` | Section heading |
@@ -345,6 +345,10 @@ interface TradeLabels {
345
345
  /** Pending sub-menu header: which side of the market the clicked price is on. */
346
346
  directionAbove: string;
347
347
  directionBelow: string;
348
+ /** SL / TP / TSL chips greyed out: no open positions on the symbol. `{symbol}` is filled in. */
349
+ actionBulkNoPositionsTitle: string;
350
+ /** SL / TP / TSL chips greyed out: buys and sells both open — one price cannot suit both. */
351
+ actionBulkMixedTitle: string;
348
352
  }
349
353
  interface DialogLabels {
350
354
  /** Chart Settings dialog. */
@@ -402,6 +406,8 @@ interface PnlPillLabels {
402
406
  hidePositionsTitle: string;
403
407
  /** ◎ tooltip — it opens the host's Bulk Levels dialog. `{count}` and `{symbol}` are filled in. */
404
408
  bulkLevelsTitle: string;
409
+ /** ◎ tooltip while greyed out because buys and sells are both open. */
410
+ bulkLevelsMixedTitle: string;
405
411
  /** Amber ✕ tooltip. */
406
412
  closeAllTitle: string;
407
413
  }
@@ -345,6 +345,10 @@ interface TradeLabels {
345
345
  /** Pending sub-menu header: which side of the market the clicked price is on. */
346
346
  directionAbove: string;
347
347
  directionBelow: string;
348
+ /** SL / TP / TSL chips greyed out: no open positions on the symbol. `{symbol}` is filled in. */
349
+ actionBulkNoPositionsTitle: string;
350
+ /** SL / TP / TSL chips greyed out: buys and sells both open — one price cannot suit both. */
351
+ actionBulkMixedTitle: string;
348
352
  }
349
353
  interface DialogLabels {
350
354
  /** Chart Settings dialog. */
@@ -402,6 +406,8 @@ interface PnlPillLabels {
402
406
  hidePositionsTitle: string;
403
407
  /** ◎ tooltip — it opens the host's Bulk Levels dialog. `{count}` and `{symbol}` are filled in. */
404
408
  bulkLevelsTitle: string;
409
+ /** ◎ tooltip while greyed out because buys and sells are both open. */
410
+ bulkLevelsMixedTitle: string;
405
411
  /** Amber ✕ tooltip. */
406
412
  closeAllTitle: string;
407
413
  }
package/dist/index.cjs CHANGED
@@ -336,6 +336,7 @@ function resolveCssVarsInTheme(theme, container) {
336
336
  drawing: resolveDrawingThemeVars(theme.drawing, resolve)
337
337
  };
338
338
  container.removeChild(tmp);
339
+ tmp.remove();
339
340
  return result;
340
341
  }
341
342
  function resolveDrawingThemeVars(drawing, resolve) {
@@ -744,6 +745,7 @@ var DEFAULT_LABELS = {
744
745
  showPositionsTitle: "Show positions on chart \xB7 drag handle to move \xB7 double-click to reset",
745
746
  hidePositionsTitle: "Hide positions on chart \xB7 drag handle to move \xB7 double-click to reset",
746
747
  bulkLevelsTitle: "Set SL \xB7 TP \xB7 TSL on all {count} {symbol} positions",
748
+ bulkLevelsMixedTitle: "Bulk levels need all {count} {symbol} positions in one direction",
747
749
  closeAllTitle: "Close all {symbol} positions"
748
750
  },
749
751
  topBar: {
@@ -932,7 +934,9 @@ var DEFAULT_LABELS = {
932
934
  actionTsl: "TSL",
933
935
  actionCloseTitle: "Close",
934
936
  directionAbove: "Above",
935
- directionBelow: "Below"
937
+ directionBelow: "Below",
938
+ actionBulkNoPositionsTitle: "No open {symbol} positions",
939
+ actionBulkMixedTitle: "Bulk levels need all {symbol} positions in one direction"
936
940
  },
937
941
  dialogs: {
938
942
  settings: {
@@ -4426,7 +4430,8 @@ function withAlpha4(color, alpha) {
4426
4430
  }
4427
4431
  const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(c);
4428
4432
  if (rgb) return `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})`;
4429
- return c;
4433
+ const pct = Math.round(Math.min(1, Math.max(0, alpha)) * 100);
4434
+ return `color-mix(in srgb, ${c} ${pct}%, transparent)`;
4430
4435
  }
4431
4436
 
4432
4437
  // src/ui/keyboardAware.ts
@@ -5426,6 +5431,14 @@ var TopBar = class {
5426
5431
  };
5427
5432
 
5428
5433
  // src/ui/AdvancedToolbar.ts
5434
+ function resolveToolbarTheme(theme) {
5435
+ if (typeof document === "undefined" || !document.body) return theme;
5436
+ try {
5437
+ return resolveCssVarsInTheme(theme, document.body);
5438
+ } catch {
5439
+ return theme;
5440
+ }
5441
+ }
5429
5442
  var TF_PILLS = ["1m", "5m", "15m", "1h", "4h", "1D", "1W"];
5430
5443
  var TF_PILLS_COMPACT = ["1m", "5m", "15m", "1h", "4h", "1D"];
5431
5444
  var TF_MORE = ["30m", "2h", "8h", "1W", "1M"];
@@ -5584,6 +5597,7 @@ var AdvancedToolbar = class {
5584
5597
  this.priceSourceOptions = [];
5585
5598
  this.currentPriceSource = "bid";
5586
5599
  this.openFlyout = null;
5600
+ theme = resolveToolbarTheme(theme);
5587
5601
  this.theme = theme;
5588
5602
  this.overflowMenu = overflowMenu;
5589
5603
  this.showSnapshotItem = showSnapshotBtn;
@@ -5923,6 +5937,7 @@ var AdvancedToolbar = class {
5923
5937
  this.volHasData = hasVol;
5924
5938
  }
5925
5939
  setTheme(theme) {
5940
+ theme = resolveToolbarTheme(theme);
5926
5941
  this.theme = theme;
5927
5942
  this.applyBarStyles(theme);
5928
5943
  this.applyFlyoutStyles(theme);
@@ -18266,18 +18281,22 @@ function summarisePositionLevels(levels, draftLabel = null) {
18266
18281
  const positions = [];
18267
18282
  let pnl = 0;
18268
18283
  let hasPnl2 = false;
18284
+ let buys = 0;
18285
+ let sells = 0;
18269
18286
  for (const level of levels) {
18270
18287
  if (level.type !== "position") continue;
18271
18288
  const position = level;
18272
18289
  if (position.entryPriceEditable) continue;
18273
18290
  if (draftLabel !== null && position.label === draftLabel) continue;
18274
18291
  positions.push(position);
18292
+ if (position.side === "buy") buys++;
18293
+ else if (position.side === "sell") sells++;
18275
18294
  if (typeof position.pnl === "number" && Number.isFinite(position.pnl)) {
18276
18295
  pnl += position.pnl;
18277
18296
  hasPnl2 = true;
18278
18297
  }
18279
18298
  }
18280
- return { count: positions.length, pnl, hasPnl: hasPnl2, positions };
18299
+ return { count: positions.length, pnl, hasPnl: hasPnl2, positions, buys, sells, mixed: buys > 0 && sells > 0 };
18281
18300
  }
18282
18301
  function formatPnlPillValue(pnl, currency) {
18283
18302
  const amount = Math.abs(pnl).toFixed(2);
@@ -18310,6 +18329,8 @@ var PnlPill = class {
18310
18329
  this.lastSummary = null;
18311
18330
  this.count = 0;
18312
18331
  this.symbol = "";
18332
+ /** Buys and sells both open — ◎ is greyed out, one price cannot suit both. */
18333
+ this.mixed = false;
18313
18334
  this.pnlTone = "none";
18314
18335
  /** Set once the user has dragged; null means the default top-right spot. */
18315
18336
  this.pos = null;
@@ -18484,6 +18505,7 @@ var PnlPill = class {
18484
18505
  update(summary, symbol = "") {
18485
18506
  this.count = summary.count;
18486
18507
  this.symbol = symbol;
18508
+ this.mixed = summary.mixed;
18487
18509
  this.lastSummary = summary;
18488
18510
  if (summary.count === 0 || this.userHidden) {
18489
18511
  this.el.style.display = "none";
@@ -18581,9 +18603,12 @@ var PnlPill = class {
18581
18603
  }
18582
18604
  applyTitles() {
18583
18605
  this.el.title = this.fill(this.expanded ? this.labels.hidePositionsTitle : this.labels.showPositionsTitle);
18584
- const bulk = this.fill(this.labels.bulkLevelsTitle);
18606
+ const bulk = this.fill(this.mixed ? this.labels.bulkLevelsMixedTitle : this.labels.bulkLevelsTitle);
18585
18607
  this.toggleBtn.title = bulk;
18586
18608
  this.toggleBtn.setAttribute("aria-label", bulk);
18609
+ this.toggleBtn.disabled = this.mixed;
18610
+ this.toggleBtn.setAttribute("aria-disabled", String(this.mixed));
18611
+ this.applyToggleColor();
18587
18612
  const closeAll = this.fill(this.labels.closeAllTitle);
18588
18613
  this.closeBtn.title = closeAll;
18589
18614
  this.closeBtn.setAttribute("aria-label", closeAll);
@@ -18619,9 +18644,10 @@ var PnlPill = class {
18619
18644
  const t = this.theme;
18620
18645
  Object.assign(this.toggleBtn.style, {
18621
18646
  color: t.ui.accent,
18622
- background: withAlpha4(t.ui.accent, this.toggleHover ? 0.2 : 0.12),
18647
+ background: withAlpha4(t.ui.accent, this.toggleHover && !this.mixed ? 0.2 : 0.12),
18623
18648
  border: "1px solid transparent",
18624
- opacity: "1"
18649
+ opacity: this.mixed ? "0.4" : "1",
18650
+ cursor: this.mixed ? "not-allowed" : "pointer"
18625
18651
  });
18626
18652
  }
18627
18653
  /* border-yellow-500/60, hover border-yellow-400 + bg-yellow-500/15; the ✕ is yellow-500. */
@@ -18839,6 +18865,7 @@ var TradeActionBar = class {
18839
18865
  const wasVisible = this.isVisible;
18840
18866
  this.price = price;
18841
18867
  if (ctx) this.ctx = ctx;
18868
+ this.applyBulkAvailability();
18842
18869
  this.closeSubmenu();
18843
18870
  this.el.style.display = "block";
18844
18871
  this.el.style.right = `${right}px`;
@@ -18897,11 +18924,34 @@ var TradeActionBar = class {
18897
18924
  this.applyCloseColor();
18898
18925
  }
18899
18926
  /* Alert amber · Pending foreground/85 → primary on hover · SL/TSL negative · TP positive. */
18927
+ /*
18928
+ * SL / TP / TSL act on every open position of the symbol. With none there is
18929
+ * nothing to act on; with buys AND sells open, one stop or target price would
18930
+ * have to sit below the market for one side and above it for the other, so the
18931
+ * host's dialog could never accept it. Both states grey the chips out and say why.
18932
+ */
18933
+ applyBulkAvailability() {
18934
+ const count = this.ctx.positionCount;
18935
+ const mixed = this.ctx.mixedSides === true;
18936
+ const unavailable = count === 0 || mixed;
18937
+ const symbol = this.ctx.symbol ?? "";
18938
+ const title = !unavailable ? "" : (count === 0 ? this.labels.actionBulkNoPositionsTitle : this.labels.actionBulkMixedTitle).replace(/\{symbol\}/g, symbol);
18939
+ for (const kind of ["sl", "tp", "tsl"]) {
18940
+ const btn = this.chips.get(kind);
18941
+ if (!btn) continue;
18942
+ btn.disabled = unavailable;
18943
+ btn.setAttribute("aria-disabled", String(unavailable));
18944
+ btn.title = title;
18945
+ btn.style.opacity = unavailable ? "0.4" : "1";
18946
+ btn.style.cursor = unavailable ? "not-allowed" : "pointer";
18947
+ this.applyChipColor(kind);
18948
+ }
18949
+ }
18900
18950
  applyChipColor(kind) {
18901
18951
  const btn = this.chips.get(kind);
18902
18952
  if (!btn) return;
18903
18953
  const t = this.theme;
18904
- const hover = this.hovered === kind;
18954
+ const hover = this.hovered === kind && !btn.disabled;
18905
18955
  let color;
18906
18956
  let hoverBg;
18907
18957
  switch (kind) {
@@ -26897,10 +26947,14 @@ var _ChartEngine = class _ChartEngine {
26897
26947
  const { price, y } = this._tradeButtonClickData;
26898
26948
  if (this.tradeActionBar) {
26899
26949
  this.tradePopover.hide();
26950
+ const positions = summarisePositionLevels(this.levels, this.draftOrderLabel);
26900
26951
  this.tradeActionBar.show(price, y, btnRight, {
26901
26952
  pricePrecision: this.dataStore.getPricePrecision(),
26902
26953
  buyOrderType: this._orderTypeForPrice("buy", price),
26903
- sellOrderType: this._orderTypeForPrice("sell", price)
26954
+ sellOrderType: this._orderTypeForPrice("sell", price),
26955
+ symbol: this.symbol,
26956
+ positionCount: positions.count,
26957
+ mixedSides: positions.mixed
26904
26958
  });
26905
26959
  return;
26906
26960
  }
@@ -27816,6 +27870,7 @@ var _ChartEngine = class _ChartEngine {
27816
27870
  onToggleExpand: () => this.togglePnlPillExpanded(),
27817
27871
  // ◎ hands every open position on the symbol to the host's Bulk Levels dialog.
27818
27872
  onBulkLevels: () => {
27873
+ if (summarisePositionLevels(this.levels, this.draftOrderLabel).mixed) return;
27819
27874
  this.emitter.emit("bulkLevelsRequested", {
27820
27875
  kind: "sl",
27821
27876
  price: null,