acttrader-charts 1.3.0-beta.18 → 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
@@ -745,6 +745,7 @@ var DEFAULT_LABELS = {
745
745
  showPositionsTitle: "Show positions on chart \xB7 drag handle to move \xB7 double-click to reset",
746
746
  hidePositionsTitle: "Hide positions on chart \xB7 drag handle to move \xB7 double-click to reset",
747
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",
748
749
  closeAllTitle: "Close all {symbol} positions"
749
750
  },
750
751
  topBar: {
@@ -933,7 +934,9 @@ var DEFAULT_LABELS = {
933
934
  actionTsl: "TSL",
934
935
  actionCloseTitle: "Close",
935
936
  directionAbove: "Above",
936
- directionBelow: "Below"
937
+ directionBelow: "Below",
938
+ actionBulkNoPositionsTitle: "No open {symbol} positions",
939
+ actionBulkMixedTitle: "Bulk levels need all {symbol} positions in one direction"
937
940
  },
938
941
  dialogs: {
939
942
  settings: {
@@ -18278,18 +18281,22 @@ function summarisePositionLevels(levels, draftLabel = null) {
18278
18281
  const positions = [];
18279
18282
  let pnl = 0;
18280
18283
  let hasPnl2 = false;
18284
+ let buys = 0;
18285
+ let sells = 0;
18281
18286
  for (const level of levels) {
18282
18287
  if (level.type !== "position") continue;
18283
18288
  const position = level;
18284
18289
  if (position.entryPriceEditable) continue;
18285
18290
  if (draftLabel !== null && position.label === draftLabel) continue;
18286
18291
  positions.push(position);
18292
+ if (position.side === "buy") buys++;
18293
+ else if (position.side === "sell") sells++;
18287
18294
  if (typeof position.pnl === "number" && Number.isFinite(position.pnl)) {
18288
18295
  pnl += position.pnl;
18289
18296
  hasPnl2 = true;
18290
18297
  }
18291
18298
  }
18292
- return { count: positions.length, pnl, hasPnl: hasPnl2, positions };
18299
+ return { count: positions.length, pnl, hasPnl: hasPnl2, positions, buys, sells, mixed: buys > 0 && sells > 0 };
18293
18300
  }
18294
18301
  function formatPnlPillValue(pnl, currency) {
18295
18302
  const amount = Math.abs(pnl).toFixed(2);
@@ -18322,6 +18329,8 @@ var PnlPill = class {
18322
18329
  this.lastSummary = null;
18323
18330
  this.count = 0;
18324
18331
  this.symbol = "";
18332
+ /** Buys and sells both open — ◎ is greyed out, one price cannot suit both. */
18333
+ this.mixed = false;
18325
18334
  this.pnlTone = "none";
18326
18335
  /** Set once the user has dragged; null means the default top-right spot. */
18327
18336
  this.pos = null;
@@ -18496,6 +18505,7 @@ var PnlPill = class {
18496
18505
  update(summary, symbol = "") {
18497
18506
  this.count = summary.count;
18498
18507
  this.symbol = symbol;
18508
+ this.mixed = summary.mixed;
18499
18509
  this.lastSummary = summary;
18500
18510
  if (summary.count === 0 || this.userHidden) {
18501
18511
  this.el.style.display = "none";
@@ -18593,9 +18603,12 @@ var PnlPill = class {
18593
18603
  }
18594
18604
  applyTitles() {
18595
18605
  this.el.title = this.fill(this.expanded ? this.labels.hidePositionsTitle : this.labels.showPositionsTitle);
18596
- const bulk = this.fill(this.labels.bulkLevelsTitle);
18606
+ const bulk = this.fill(this.mixed ? this.labels.bulkLevelsMixedTitle : this.labels.bulkLevelsTitle);
18597
18607
  this.toggleBtn.title = bulk;
18598
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();
18599
18612
  const closeAll = this.fill(this.labels.closeAllTitle);
18600
18613
  this.closeBtn.title = closeAll;
18601
18614
  this.closeBtn.setAttribute("aria-label", closeAll);
@@ -18631,9 +18644,10 @@ var PnlPill = class {
18631
18644
  const t = this.theme;
18632
18645
  Object.assign(this.toggleBtn.style, {
18633
18646
  color: t.ui.accent,
18634
- 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),
18635
18648
  border: "1px solid transparent",
18636
- opacity: "1"
18649
+ opacity: this.mixed ? "0.4" : "1",
18650
+ cursor: this.mixed ? "not-allowed" : "pointer"
18637
18651
  });
18638
18652
  }
18639
18653
  /* border-yellow-500/60, hover border-yellow-400 + bg-yellow-500/15; the ✕ is yellow-500. */
@@ -18851,6 +18865,7 @@ var TradeActionBar = class {
18851
18865
  const wasVisible = this.isVisible;
18852
18866
  this.price = price;
18853
18867
  if (ctx) this.ctx = ctx;
18868
+ this.applyBulkAvailability();
18854
18869
  this.closeSubmenu();
18855
18870
  this.el.style.display = "block";
18856
18871
  this.el.style.right = `${right}px`;
@@ -18909,11 +18924,34 @@ var TradeActionBar = class {
18909
18924
  this.applyCloseColor();
18910
18925
  }
18911
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
+ }
18912
18950
  applyChipColor(kind) {
18913
18951
  const btn = this.chips.get(kind);
18914
18952
  if (!btn) return;
18915
18953
  const t = this.theme;
18916
- const hover = this.hovered === kind;
18954
+ const hover = this.hovered === kind && !btn.disabled;
18917
18955
  let color;
18918
18956
  let hoverBg;
18919
18957
  switch (kind) {
@@ -26909,10 +26947,14 @@ var _ChartEngine = class _ChartEngine {
26909
26947
  const { price, y } = this._tradeButtonClickData;
26910
26948
  if (this.tradeActionBar) {
26911
26949
  this.tradePopover.hide();
26950
+ const positions = summarisePositionLevels(this.levels, this.draftOrderLabel);
26912
26951
  this.tradeActionBar.show(price, y, btnRight, {
26913
26952
  pricePrecision: this.dataStore.getPricePrecision(),
26914
26953
  buyOrderType: this._orderTypeForPrice("buy", price),
26915
- sellOrderType: this._orderTypeForPrice("sell", price)
26954
+ sellOrderType: this._orderTypeForPrice("sell", price),
26955
+ symbol: this.symbol,
26956
+ positionCount: positions.count,
26957
+ mixedSides: positions.mixed
26916
26958
  });
26917
26959
  return;
26918
26960
  }
@@ -27828,6 +27870,7 @@ var _ChartEngine = class _ChartEngine {
27828
27870
  onToggleExpand: () => this.togglePnlPillExpanded(),
27829
27871
  // ◎ hands every open position on the symbol to the host's Bulk Levels dialog.
27830
27872
  onBulkLevels: () => {
27873
+ if (summarisePositionLevels(this.levels, this.draftOrderLabel).mixed) return;
27831
27874
  this.emitter.emit("bulkLevelsRequested", {
27832
27875
  kind: "sl",
27833
27876
  price: null,