@stocksharp/trading-controls 1.2.0 → 1.3.0

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.
Files changed (59) hide show
  1. package/README.md +95 -16
  2. package/dist/esm/black-scholes.js +147 -0
  3. package/dist/esm/black-scholes.js.map +1 -0
  4. package/dist/esm/control-types.js +4 -0
  5. package/dist/esm/control-types.js.map +1 -1
  6. package/dist/esm/index.js +18 -0
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/esm/log-monitor-widget.js +265 -0
  9. package/dist/esm/log-monitor-widget.js.map +1 -0
  10. package/dist/esm/log-tree.js +96 -0
  11. package/dist/esm/log-tree.js.map +1 -0
  12. package/dist/esm/option-desk-widget.js +322 -0
  13. package/dist/esm/option-desk-widget.js.map +1 -0
  14. package/dist/esm/pnl-curve.js +129 -0
  15. package/dist/esm/pnl-curve.js.map +1 -0
  16. package/dist/esm/statistics-widget.js +194 -0
  17. package/dist/esm/statistics-widget.js.map +1 -0
  18. package/dist/esm/strategies-widget.js +348 -0
  19. package/dist/esm/strategies-widget.js.map +1 -0
  20. package/dist/sstradingcontrols.js +1307 -47
  21. package/dist/sstradingcontrols.js.map +4 -4
  22. package/dist/types/black-scholes.d.ts +28 -0
  23. package/dist/types/black-scholes.d.ts.map +1 -0
  24. package/dist/types/control-types.d.ts +4 -0
  25. package/dist/types/control-types.d.ts.map +1 -1
  26. package/dist/types/index.d.ts +16 -1
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/dist/types/log-monitor-widget.d.ts +42 -0
  29. package/dist/types/log-monitor-widget.d.ts.map +1 -0
  30. package/dist/types/log-tree.d.ts +34 -0
  31. package/dist/types/log-tree.d.ts.map +1 -0
  32. package/dist/types/option-desk-widget.d.ts +68 -0
  33. package/dist/types/option-desk-widget.d.ts.map +1 -0
  34. package/dist/types/pnl-curve.d.ts +43 -0
  35. package/dist/types/pnl-curve.d.ts.map +1 -0
  36. package/dist/types/statistics-widget.d.ts +29 -0
  37. package/dist/types/statistics-widget.d.ts.map +1 -0
  38. package/dist/types/strategies-widget.d.ts +63 -0
  39. package/dist/types/strategies-widget.d.ts.map +1 -0
  40. package/dist/types/trading-data.d.ts +9 -0
  41. package/dist/types/trading-data.d.ts.map +1 -1
  42. package/package.json +27 -2
  43. package/screenshots/log-monitor.png +0 -0
  44. package/screenshots/option-desk.png +0 -0
  45. package/screenshots/panels.jpg +0 -0
  46. package/screenshots/statistics.png +0 -0
  47. package/screenshots/strategies.png +0 -0
  48. package/src/black-scholes.ts +199 -0
  49. package/src/control-types.ts +4 -0
  50. package/src/index.ts +41 -0
  51. package/src/log-monitor-widget.ts +312 -0
  52. package/src/log-tree.ts +131 -0
  53. package/src/option-desk-widget.ts +422 -0
  54. package/src/pnl-curve.ts +204 -0
  55. package/src/statistics-widget.ts +226 -0
  56. package/src/strategies-widget.ts +435 -0
  57. package/src/trading-data.ts +22 -0
  58. package/styles/trading-controls.css +356 -0
  59. package/translation-keys.json +70 -1
@@ -25,7 +25,11 @@ var SSTradingControls = (() => {
25
25
  __export(index_exports, {
26
26
  ActiveOrdersWidget: () => ActiveOrdersWidget,
27
27
  ControlTypes: () => ControlTypes,
28
+ LogLevels: () => LogLevels,
29
+ LogMonitorWidget: () => LogMonitorWidget,
28
30
  MarketDataLevels: () => MarketDataLevels,
31
+ OptionDeskWidget: () => OptionDeskWidget,
32
+ OptionTypes: () => OptionTypes,
29
33
  OrderBookWidget: () => OrderBookWidget,
30
34
  OrderEntrySides: () => OrderEntrySides,
31
35
  OrderEntryTypes: () => OrderEntryTypes,
@@ -33,22 +37,44 @@ var SSTradingControls = (() => {
33
37
  OrderStates: () => OrderStates,
34
38
  PRESENTATION_CLASSES: () => PRESENTATION_CLASSES,
35
39
  PositionsWidget: () => PositionsWidget,
40
+ StatisticsWidget: () => StatisticsWidget,
41
+ StrategiesWidget: () => StrategiesWidget,
42
+ StrategyStates: () => StrategyStates,
36
43
  TradeFeedWidget: () => TradeFeedWidget,
37
44
  TradeHistoryWidget: () => TradeHistoryWidget,
38
45
  WatchlistWidget: () => WatchlistWidget,
39
46
  aggregateBubbles: () => aggregateBubbles,
40
47
  assertHost: () => assertHost,
48
+ buildLogTree: () => buildLogTree,
41
49
  cleanRejectReason: () => cleanRejectReason,
50
+ compressPnl: () => compressPnl,
51
+ d1: () => d1,
52
+ d2: () => d2,
53
+ drawPnlCurve: () => drawPnlCurve,
42
54
  formatPnl: () => formatPnl,
43
55
  formatPrice: () => formatPrice,
44
56
  formatQty: () => formatQty,
57
+ formatStatistic: () => formatStatistic,
45
58
  formatTime: () => formatTime,
59
+ greekPlaces: () => greekPlaces,
60
+ greekScales: () => greekScales,
61
+ greeks: () => greeks,
62
+ impliedVolatility: () => impliedVolatility,
63
+ keepLog: () => keepLog,
46
64
  layoutBubbles: () => layoutBubbles,
47
65
  makeElement: () => makeElement,
66
+ makeGridMenu: () => makeGridMenu,
48
67
  makeIcon: () => makeIcon,
49
68
  makeIconButton: () => makeIconButton,
50
69
  makePanelId: () => makePanelId,
51
- makePanelRoot: () => makePanelRoot
70
+ makePanelRoot: () => makePanelRoot,
71
+ normalCdf: () => normalCdf,
72
+ normalPdf: () => normalPdf,
73
+ pnlCurve: () => pnlCurve,
74
+ premium: () => premium,
75
+ scaleChain: () => scaleChain,
76
+ sideGreeks: () => sideGreeks,
77
+ subtreeOf: () => subtreeOf
52
78
  });
53
79
 
54
80
  // src/trading-host.ts
@@ -120,13 +146,17 @@ var SSTradingControls = (() => {
120
146
  Positions: "positions",
121
147
  OrderBook: "orderbook",
122
148
  TradeFeed: "tradefeed",
123
- OrderEntry: "orderEntry"
149
+ OrderEntry: "orderEntry",
150
+ Statistics: "statistics",
151
+ LogMonitor: "logMonitor",
152
+ Strategies: "strategies",
153
+ OptionDesk: "optionDesk"
124
154
  };
125
155
 
126
156
  // src/formatters.ts
127
- function formatPrice(price) {
128
- if (price == null || isNaN(Number(price))) return "--";
129
- const n = Number(price);
157
+ function formatPrice(price2) {
158
+ if (price2 == null || isNaN(Number(price2))) return "--";
159
+ const n = Number(price2);
130
160
  const a = Math.abs(n);
131
161
  let prec = 2;
132
162
  if (a < 1) prec = 4;
@@ -815,6 +845,13 @@ var SSTradingControls = (() => {
815
845
  copyRows: (count) => `Copy selected rows (${count})`,
816
846
  exportXlsx: "Export to .xlsx"
817
847
  };
848
+ var CONTROL_TAGS = ["SELECT", "INPUT", "BUTTON", "TEXTAREA", "A", "OPTION", "LABEL"];
849
+ function isControl(event) {
850
+ const target = event.target;
851
+ if (target === null || target === void 0) return false;
852
+ if (target.isContentEditable === true) return true;
853
+ return CONTROL_TAGS.includes(String(target.tagName ?? "").toUpperCase());
854
+ }
818
855
  var _DataGrid = class _DataGrid {
819
856
  constructor(options) {
820
857
  __publicField(this, "options");
@@ -1727,6 +1764,7 @@ var SSTradingControls = (() => {
1727
1764
  tr.addEventListener("mousedown", (event) => {
1728
1765
  const button = event.button;
1729
1766
  if (button !== void 0 && button !== 0) return;
1767
+ if (isControl(event)) return;
1730
1768
  event.preventDefault();
1731
1769
  _DataGrid._active = this;
1732
1770
  this._clickRow(key, event);
@@ -1788,6 +1826,7 @@ var SSTradingControls = (() => {
1788
1826
  if (group) group.rows.push(row);
1789
1827
  else groups.set(key, { label: col ? this._displayText(col, row) : key, rows: [row] });
1790
1828
  }
1829
+ if (this.options.groupOrder === "rows") return groups;
1791
1830
  return new Map([...groups.entries()].sort((a, b) => this._collator.compare(a[1].label, b[1].label)));
1792
1831
  }
1793
1832
  _groupRow(key, label, count) {
@@ -1802,6 +1841,8 @@ var SSTradingControls = (() => {
1802
1841
  else td.appendChild(content);
1803
1842
  tr.appendChild(td);
1804
1843
  tr.addEventListener("click", () => this.toggleGroup(key));
1844
+ tr.style.cursor = "pointer";
1845
+ tr.style.userSelect = "none";
1805
1846
  return tr;
1806
1847
  }
1807
1848
  _emptyRow() {
@@ -2792,8 +2833,8 @@ var SSTradingControls = (() => {
2792
2833
  if (previous) this._grid.rowElement(String(previous))?.classList.remove("wl-current");
2793
2834
  if (symbol) this._grid.rowElement(String(symbol))?.classList.add("wl-current");
2794
2835
  }
2795
- onPriceUpdate(symbol, price) {
2796
- if (price == null || !isFinite(price)) return;
2836
+ onPriceUpdate(symbol, price2) {
2837
+ if (price2 == null || !isFinite(price2)) return;
2797
2838
  let full = symbol;
2798
2839
  if (!full.includes("@")) {
2799
2840
  const match = this.instruments.find((i) => String(i.symbol).split("@")[0] === symbol);
@@ -2808,11 +2849,11 @@ var SSTradingControls = (() => {
2808
2849
  baseline = stored ? Number(stored) : void 0;
2809
2850
  }
2810
2851
  if (baseline == null || !isFinite(baseline) || baseline === 0) {
2811
- baseline = price;
2812
- this._host.cache.set(baselineKey, String(price));
2852
+ baseline = price2;
2853
+ this._host.cache.set(baselineKey, String(price2));
2813
2854
  }
2814
- const chgPct = baseline ? (price - baseline) / baseline * 100 : 0;
2815
- this.stats.set(full, { ...cur || {}, lastPrice: price, baseline, chgPct });
2855
+ const chgPct = baseline ? (price2 - baseline) / baseline * 100 : 0;
2856
+ this.stats.set(full, { ...cur || {}, lastPrice: price2, baseline, chgPct });
2816
2857
  this._updateRow(full, prev);
2817
2858
  }
2818
2859
  static _baselineKey(symbol) {
@@ -3018,9 +3059,9 @@ var SSTradingControls = (() => {
3018
3059
  if (!this._host.isPrimary) return;
3019
3060
  this._host.ticker.publish(this._visible().map((i) => String(i.symbol)), this.stats);
3020
3061
  }
3021
- static _priceText(price) {
3022
- const n = Number(price);
3023
- if (price == null || !isFinite(n) || n === 0) return "--";
3062
+ static _priceText(price2) {
3063
+ const n = Number(price2);
3064
+ if (price2 == null || !isFinite(n) || n === 0) return "--";
3024
3065
  if (n >= 1) return n.toFixed(2);
3025
3066
  if (n >= 0.01) return n.toFixed(4);
3026
3067
  return n.toFixed(6);
@@ -3292,15 +3333,15 @@ var SSTradingControls = (() => {
3292
3333
  }
3293
3334
  /// A reference price off the tape. Both columns follow the market while the
3294
3335
  /// form is untouched; the field the trader is typing in is left alone.
3295
- setLimitPrice(price) {
3296
- this._lastPrice = price;
3297
- this._writePrices(this._bestBid > 0 ? this._bestBid : price, this._bestAsk > 0 ? this._bestAsk : price, false);
3336
+ setLimitPrice(price2) {
3337
+ this._lastPrice = price2;
3338
+ this._writePrices(this._bestBid > 0 ? this._bestBid : price2, this._bestAsk > 0 ? this._bestAsk : price2, false);
3298
3339
  }
3299
3340
  /// The same reference price, force-written: an explicit host gesture
3300
3341
  /// ("price this order here") outranks whatever was typed.
3301
- seedLimitPrice(price) {
3302
- this._lastPrice = price;
3303
- this._writePrices(this._bestBid > 0 ? this._bestBid : price, this._bestAsk > 0 ? this._bestAsk : price, true);
3342
+ seedLimitPrice(price2) {
3343
+ this._lastPrice = price2;
3344
+ this._writePrices(this._bestBid > 0 ? this._bestBid : price2, this._bestAsk > 0 ? this._bestAsk : price2, true);
3304
3345
  }
3305
3346
  /// Best bid and offer. The passive seed for a limit form is Buy = best bid
3306
3347
  /// and Sell = best ask — joining the book on each side, so the spread in the
@@ -3325,10 +3366,10 @@ var SSTradingControls = (() => {
3325
3366
  /// Force one column's price and let ticks resume updating it afterwards —
3326
3367
  /// what a click on a book level or a chart price means. Quantized to the
3327
3368
  /// instrument's tick, so a raw chart coordinate does not fail validation.
3328
- setPrice(side, price) {
3369
+ setPrice(side, price2) {
3329
3370
  const input = this._input(side, PRICE_INPUT);
3330
3371
  if (!input) return;
3331
- const text = this._formatPriceForInput(price);
3372
+ const text = this._formatPriceForInput(price2);
3332
3373
  if (!text) return;
3333
3374
  input.dataset.userEdited = "0";
3334
3375
  input.value = text;
@@ -3560,10 +3601,10 @@ var SSTradingControls = (() => {
3560
3601
  this._writePrice(OrderEntrySides.Sell, sellPrice, reset);
3561
3602
  for (const side of _OrderEntryWidget.SIDES) this._recalculate(side);
3562
3603
  }
3563
- _writePrice(side, price, reset) {
3604
+ _writePrice(side, price2, reset) {
3564
3605
  const input = this._input(side, PRICE_INPUT);
3565
3606
  if (!input) return;
3566
- const text = this._formatPriceForInput(price);
3607
+ const text = this._formatPriceForInput(price2);
3567
3608
  if (!text) return;
3568
3609
  if (reset) {
3569
3610
  input.dataset.userEdited = "0";
@@ -3576,8 +3617,8 @@ var SSTradingControls = (() => {
3576
3617
  }
3577
3618
  // Round a price to the instrument's tick and cut the float tail: a mid price
3578
3619
  // of (bid+ask)/2 otherwise reaches the field as 0.44625000000000004.
3579
- _formatPriceForInput(price) {
3580
- const n = Number(price);
3620
+ _formatPriceForInput(price2) {
3621
+ const n = Number(price2);
3581
3622
  if (!isFinite(n)) return "";
3582
3623
  const tickSize = Number(this._instrument?.tickSize) || 0;
3583
3624
  if (tickSize > 0)
@@ -3595,16 +3636,16 @@ var SSTradingControls = (() => {
3595
3636
  const total = this._cols[side]?.querySelector(TOTAL_VALUE);
3596
3637
  if (!total) return;
3597
3638
  const quantity = this._number(side, QTY_INPUT) ?? 0;
3598
- let price;
3639
+ let price2;
3599
3640
  if (this._orderType === OrderEntryTypes.Limit || this._orderType === OrderEntryTypes.StopLimit) {
3600
- price = this._number(side, PRICE_INPUT) ?? 0;
3641
+ price2 = this._number(side, PRICE_INPUT) ?? 0;
3601
3642
  } else if (this._orderType === OrderEntryTypes.Stop) {
3602
- price = this._number(side, STOP_INPUT) ?? this._lastPrice;
3643
+ price2 = this._number(side, STOP_INPUT) ?? this._lastPrice;
3603
3644
  } else {
3604
3645
  const bbo = side === OrderEntrySides.Buy ? this._bestAsk : this._bestBid;
3605
- price = bbo > 0 ? bbo : this._lastPrice;
3646
+ price2 = bbo > 0 ? bbo : this._lastPrice;
3606
3647
  }
3607
- total.textContent = quantity > 0 && price > 0 ? (quantity * price).toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "--";
3648
+ total.textContent = quantity > 0 && price2 > 0 ? (quantity * price2).toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : "--";
3608
3649
  }
3609
3650
  // The estimate line doubles as the validation message and the submit button
3610
3651
  // follows it. A disabled pad reads as invalid for the same reason: there is
@@ -3634,20 +3675,20 @@ var SSTradingControls = (() => {
3634
3675
  var OrderEntryWidget = _OrderEntryWidget;
3635
3676
 
3636
3677
  // src/orderbook-depth.ts
3637
- function depthSide(levels, direction, geometry) {
3678
+ function depthSide(levels, direction2, geometry) {
3638
3679
  if (levels.length === 0) return null;
3639
3680
  const best = levels[0].price;
3640
3681
  const worst = levels[levels.length - 1].price;
3641
3682
  const span = Math.abs(best - worst);
3642
3683
  if (!(span > 0)) return null;
3643
3684
  const reach = geometry.halfWidth - geometry.pad;
3644
- const points = [{ x: geometry.midX + direction * geometry.pad, cumulative: 0 }];
3685
+ const points = [{ x: geometry.midX + direction2 * geometry.pad, cumulative: 0 }];
3645
3686
  let cumulative = 0;
3646
3687
  for (const level of levels) {
3647
3688
  cumulative += level.quantity;
3648
3689
  const distance = Math.abs(level.price - best);
3649
3690
  points.push({
3650
- x: geometry.midX + direction * (geometry.pad + distance / span * reach),
3691
+ x: geometry.midX + direction2 * (geometry.pad + distance / span * reach),
3651
3692
  cumulative
3652
3693
  });
3653
3694
  }
@@ -4009,11 +4050,11 @@ var SSTradingControls = (() => {
4009
4050
  const target = e.target;
4010
4051
  const row = target?.closest(".ob-row");
4011
4052
  if (!row) return;
4012
- const price = Number(row.dataset.price);
4013
- if (!Number.isFinite(price)) return;
4053
+ const price2 = Number(row.dataset.price);
4054
+ if (!Number.isFinite(price2)) return;
4014
4055
  const side = row.dataset.side === "buy" ? 0 : 1;
4015
- if (e.ctrlKey || e.metaKey) this._deps.onPriceExecuted(price, side);
4016
- else this._deps.onPriceSelected(price, side);
4056
+ if (e.ctrlKey || e.metaKey) this._deps.onPriceExecuted(price2, side);
4057
+ else this._deps.onPriceSelected(price2, side);
4017
4058
  });
4018
4059
  }
4019
4060
  _refreshDepthButtons() {
@@ -4072,8 +4113,8 @@ var SSTradingControls = (() => {
4072
4113
  static _deepest() {
4073
4114
  return _OrderBookWidget.DEPTHS[_OrderBookWidget.DEPTHS.length - 1];
4074
4115
  }
4075
- static _sorted(ledger, direction) {
4076
- return [...ledger.entries()].filter(([price, quantity]) => Number.isFinite(price) && quantity > 0).sort((left, right) => direction * (left[0] - right[0])).map(([price, quantity]) => ({ price, quantity }));
4116
+ static _sorted(ledger, direction2) {
4117
+ return [...ledger.entries()].filter(([price2, quantity]) => Number.isFinite(price2) && quantity > 0).sort((left, right) => direction2 * (left[0] - right[0])).map(([price2, quantity]) => ({ price: price2, quantity }));
4077
4118
  }
4078
4119
  _applySnapshotSide(ledger, levels, side, symbol, sequence) {
4079
4120
  for (const level of levels || []) {
@@ -4113,8 +4154,8 @@ var SSTradingControls = (() => {
4113
4154
  if (this._bidsByPrice.size === 0 || this._asksByPrice.size === 0) return;
4114
4155
  let bestBid = -Infinity;
4115
4156
  let bestAsk = Infinity;
4116
- for (const price of this._bidsByPrice.keys()) if (price > bestBid) bestBid = price;
4117
- for (const price of this._asksByPrice.keys()) if (price < bestAsk) bestAsk = price;
4157
+ for (const price2 of this._bidsByPrice.keys()) if (price2 > bestBid) bestBid = price2;
4158
+ for (const price2 of this._asksByPrice.keys()) if (price2 < bestAsk) bestAsk = price2;
4118
4159
  if (bestBid >= bestAsk) {
4119
4160
  this._anomaly(`crossed book: bid=${bestBid} >= ask=${bestAsk} sym=${symbol} seq=${sequence} ${wasSnapshot ? "snapshot" : "diff"}`);
4120
4161
  }
@@ -4250,8 +4291,8 @@ var SSTradingControls = (() => {
4250
4291
  }
4251
4292
  return { bids, asks };
4252
4293
  }
4253
- static _priceKey(price) {
4254
- return Number(price).toFixed(8);
4294
+ static _priceKey(price2) {
4295
+ return Number(price2).toFixed(8);
4255
4296
  }
4256
4297
  // The bid/ask split under the ladder, as one measurement the stylesheet
4257
4298
  // turns into two widths.
@@ -4354,6 +4395,1225 @@ var SSTradingControls = (() => {
4354
4395
  _OrderBookWidget.DEPTHCHART_KEY = "terminal.obDepthChart";
4355
4396
  var OrderBookWidget = _OrderBookWidget;
4356
4397
 
4398
+ // src/pnl-curve.ts
4399
+ function compressPnl(points, count) {
4400
+ if (points.length <= count || count < 2) return points.slice();
4401
+ const first = points[0];
4402
+ const last = points[points.length - 1];
4403
+ const span = last.time - first.time;
4404
+ if (!(span > 0)) {
4405
+ const step = (points.length - 1) / (count - 1);
4406
+ return Array.from({ length: count }, (_, i) => points[Math.round(i * step)]);
4407
+ }
4408
+ const buckets = count - 1;
4409
+ const out = [first];
4410
+ let bucket = 0;
4411
+ for (let i = 1; i < points.length; i++) {
4412
+ const index = Math.min(buckets - 1, Math.floor((points[i].time - first.time) / span * buckets));
4413
+ if (index > bucket) {
4414
+ out.push(points[i - 1]);
4415
+ bucket = index;
4416
+ }
4417
+ }
4418
+ if (out[out.length - 1] !== last) out.push(last);
4419
+ return out;
4420
+ }
4421
+ function pnlCurve(points, box) {
4422
+ const clean = points.filter((p) => p !== null && p !== void 0 && Number.isFinite(p.time) && Number.isFinite(p.value)).slice().sort((a, b) => a.time - b.time);
4423
+ if (clean.length < 2) return null;
4424
+ const drawable = Math.max(2, Math.round(box.width - box.padX * 2));
4425
+ const shown = compressPnl(clean, drawable);
4426
+ const values = shown.map((p) => p.value);
4427
+ const min = Math.min(0, ...values);
4428
+ const max = Math.max(0, ...values);
4429
+ const left = box.padX;
4430
+ const right = box.width - box.padX;
4431
+ const top = box.padY;
4432
+ const bottom = box.height - box.padY;
4433
+ const firstTime = shown[0].time;
4434
+ const lastTime = shown[shown.length - 1].time;
4435
+ const timeSpan = lastTime - firstTime;
4436
+ const valueSpan = max - min;
4437
+ const x = (time) => timeSpan === 0 ? left : left + (time - firstTime) / timeSpan * (right - left);
4438
+ const y = (value) => valueSpan === 0 ? top : bottom - (value - min) / valueSpan * (bottom - top);
4439
+ const curve = shown.map((p) => [x(p.time), y(p.value)]);
4440
+ const zeroY = y(0);
4441
+ return {
4442
+ points: curve,
4443
+ // Closed along the baseline rather than along the foot of the box: a run entirely above
4444
+ // water fills the gap between itself and zero, and nothing below it.
4445
+ area: [...curve, [curve[curve.length - 1][0], zeroY], [curve[0][0], zeroY]],
4446
+ zeroY,
4447
+ min,
4448
+ max,
4449
+ positive: shown[shown.length - 1].value >= 0
4450
+ };
4451
+ }
4452
+ function drawPnlCurve(ctx, curve, box, style) {
4453
+ const colour = curve.positive ? style.up : style.down;
4454
+ ctx.clearRect(0, 0, box.width, box.height);
4455
+ ctx.setLineDash([2, 3]);
4456
+ ctx.strokeStyle = style.baseline;
4457
+ ctx.lineWidth = 1;
4458
+ ctx.beginPath();
4459
+ ctx.moveTo(box.padX, curve.zeroY);
4460
+ ctx.lineTo(box.width - box.padX, curve.zeroY);
4461
+ ctx.stroke();
4462
+ ctx.setLineDash([]);
4463
+ ctx.globalAlpha = style.fillOpacity;
4464
+ ctx.fillStyle = colour;
4465
+ ctx.beginPath();
4466
+ ctx.moveTo(curve.area[0][0], curve.area[0][1]);
4467
+ for (const [px, py] of curve.area.slice(1)) ctx.lineTo(px, py);
4468
+ ctx.closePath();
4469
+ ctx.fill();
4470
+ ctx.globalAlpha = 1;
4471
+ ctx.strokeStyle = colour;
4472
+ ctx.lineWidth = style.lineWidth;
4473
+ ctx.beginPath();
4474
+ ctx.moveTo(curve.points[0][0], curve.points[0][1]);
4475
+ for (const [px, py] of curve.points.slice(1)) ctx.lineTo(px, py);
4476
+ ctx.stroke();
4477
+ }
4478
+
4479
+ // src/black-scholes.ts
4480
+ var OptionTypes = {
4481
+ Call: "call",
4482
+ Put: "put"
4483
+ };
4484
+ var SQRT_2PI = Math.sqrt(2 * Math.PI);
4485
+ var DAYS_IN_YEAR = 365;
4486
+ function normalCdf(x) {
4487
+ const z = Math.abs(x);
4488
+ if (z > 37) return x > 0 ? 1 : 0;
4489
+ const e = Math.exp(-(z * z) / 2);
4490
+ let tail;
4491
+ if (z < 7.07106781186547) {
4492
+ let b = 0.0352624965998911 * z + 0.700383064443688;
4493
+ b = b * z + 6.37396220353165;
4494
+ b = b * z + 33.912866078383;
4495
+ b = b * z + 112.079291497871;
4496
+ b = b * z + 221.213596169931;
4497
+ b = b * z + 220.206867912376;
4498
+ let d = 0.0883883476483184 * z + 1.75566716318264;
4499
+ d = d * z + 16.064177579207;
4500
+ d = d * z + 86.7807322029461;
4501
+ d = d * z + 296.564248779674;
4502
+ d = d * z + 637.333633378831;
4503
+ d = d * z + 793.826512519948;
4504
+ d = d * z + 440.413735824752;
4505
+ tail = e * b / d;
4506
+ } else {
4507
+ let b = z + 0.65;
4508
+ b = z + 4 / b;
4509
+ b = z + 3 / b;
4510
+ b = z + 2 / b;
4511
+ b = z + 1 / b;
4512
+ tail = e / (b * 2.506628274631);
4513
+ }
4514
+ return x > 0 ? 1 - tail : tail;
4515
+ }
4516
+ function normalPdf(x) {
4517
+ return Math.exp(-0.5 * x * x) / SQRT_2PI;
4518
+ }
4519
+ function d1(inputs) {
4520
+ const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
4521
+ const spread = deviation * Math.sqrt(timeToExpiry);
4522
+ if (spread === 0 || assetPrice <= 0 || strike <= 0) return 0;
4523
+ return (Math.log(assetPrice / strike) + (riskFree - dividend + deviation * deviation / 2) * timeToExpiry) / spread;
4524
+ }
4525
+ function d2(inputs) {
4526
+ const spread = inputs.deviation * Math.sqrt(inputs.timeToExpiry);
4527
+ return spread === 0 ? 0 : d1(inputs) - spread;
4528
+ }
4529
+ function premium(type, inputs) {
4530
+ const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
4531
+ if (timeToExpiry <= 0 || deviation <= 0)
4532
+ return Math.max(0, type === OptionTypes.Call ? assetPrice - strike : strike - assetPrice);
4533
+ const a = d1(inputs);
4534
+ const b = d2(inputs);
4535
+ const carried = assetPrice * Math.exp(-dividend * timeToExpiry);
4536
+ const discounted = strike * Math.exp(-riskFree * timeToExpiry);
4537
+ return type === OptionTypes.Call ? carried * normalCdf(a) - discounted * normalCdf(b) : discounted * normalCdf(-b) - carried * normalCdf(-a);
4538
+ }
4539
+ function greeks(type, inputs) {
4540
+ const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
4541
+ const sign = type === OptionTypes.Call ? 1 : -1;
4542
+ if (timeToExpiry <= 0 || deviation <= 0 || assetPrice <= 0) {
4543
+ const inTheMoney = type === OptionTypes.Call ? assetPrice > strike : assetPrice < strike;
4544
+ return { delta: inTheMoney ? sign : 0, gamma: 0, vega: 0, theta: 0, rho: 0 };
4545
+ }
4546
+ const a = d1(inputs);
4547
+ const b = d2(inputs);
4548
+ const sqrtT = Math.sqrt(timeToExpiry);
4549
+ const density = normalPdf(a);
4550
+ const carry = Math.exp(-dividend * timeToExpiry);
4551
+ const discount = Math.exp(-riskFree * timeToExpiry);
4552
+ const delta = sign * carry * normalCdf(sign * a);
4553
+ const gamma = carry * density / (assetPrice * deviation * sqrtT);
4554
+ const vega = assetPrice * carry * density * sqrtT * 0.01;
4555
+ const rho = sign * strike * timeToExpiry * discount * normalCdf(sign * b) * 0.01;
4556
+ const theta = (-(assetPrice * carry * density * deviation) / (2 * sqrtT) - sign * riskFree * strike * discount * normalCdf(sign * b) + sign * dividend * assetPrice * carry * normalCdf(sign * a)) / DAYS_IN_YEAR;
4557
+ return { delta, gamma, vega, theta, rho };
4558
+ }
4559
+ function impliedVolatility(type, inputs, price2) {
4560
+ if (!(price2 > 0) || inputs.timeToExpiry <= 0) return null;
4561
+ const at = (deviation) => premium(type, { ...inputs, deviation });
4562
+ let low = 1e-6;
4563
+ let high = 5;
4564
+ if (price2 < at(low) || price2 > at(high)) return null;
4565
+ for (let step = 0; step < 60; step++) {
4566
+ const mid = (low + high) / 2;
4567
+ if (at(mid) < price2) low = mid;
4568
+ else high = mid;
4569
+ }
4570
+ return (low + high) / 2;
4571
+ }
4572
+
4573
+ // src/option-desk-widget.ts
4574
+ var _OptionDeskWidget = class _OptionDeskWidget {
4575
+ static create(hostEl, state, deps) {
4576
+ const host = assertHost(deps?.host, "OptionDeskWidget");
4577
+ const root = _OptionDeskWidget._buildRoot(host);
4578
+ root.id = makePanelId(_OptionDeskWidget.TYPE);
4579
+ hostEl.appendChild(root);
4580
+ return new _OptionDeskWidget(root, state || {}, deps);
4581
+ }
4582
+ static _buildRoot(host) {
4583
+ const title = host.t("OptionDesk");
4584
+ return makePanelRoot("option-desk-panel", title, [
4585
+ makeElement("div", "panel-header", {}, [
4586
+ makeElement("span", "", {}, [title]),
4587
+ makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
4588
+ ]),
4589
+ makeElement("div", "panel-body panel-body-with-rail", {}, [
4590
+ makeElement("div", "panel-body-content", {}, [
4591
+ makeElement("table", "terminal-table option-desk-table", { role: "table", "aria-label": host.t("OptionChain") }, [
4592
+ makeElement("thead", "", {}, []),
4593
+ makeElement("tbody", "option-desk-body", {}, [])
4594
+ ])
4595
+ ]),
4596
+ makeElement("div", "panel-rail", { role: "toolbar", "aria-label": host.t("OptionDeskActions") }, [
4597
+ makeIconButton("bt-icon-btn panel-export-btn", host.t("ExportToExcel"), "bi-file-earmark-spreadsheet", {})
4598
+ ])
4599
+ ])
4600
+ ]);
4601
+ }
4602
+ constructor(rootEl, _state, deps) {
4603
+ this._host = assertHost(deps?.host, "OptionDeskWidget");
4604
+ this.rootEl = rootEl;
4605
+ this.el = this.rootEl.querySelector(".option-desk-body");
4606
+ this._closeBtn = this.rootEl.querySelector(".panel-close-btn");
4607
+ this._exportBtn = this.rootEl.querySelector(".panel-export-btn");
4608
+ this._rows = [];
4609
+ this._context = {};
4610
+ this._places = greekScales([], {});
4611
+ this._closeBtn?.addEventListener("click", (e) => {
4612
+ e.preventDefault();
4613
+ this._host.close();
4614
+ });
4615
+ this._exportBtn?.addEventListener("click", (e) => {
4616
+ e.preventDefault();
4617
+ this._export();
4618
+ });
4619
+ const head = this.rootEl.querySelector(".option-desk-table thead");
4620
+ this._grid = head && this.el ? new DataGrid({
4621
+ head,
4622
+ body: this.el,
4623
+ columns: this._columns(),
4624
+ // By strike, ascending. A chain has exactly one order and it is not negotiable:
4625
+ // the strip is read as a ladder, and re-sorting it by any column destroys that.
4626
+ defaultSort: { col: "strike", dir: "asc" },
4627
+ rowKey: (r) => String(r.strike),
4628
+ emptyText: this._host.t("NoOptions"),
4629
+ rowClass: (r) => this._rowClass(r),
4630
+ contextMenu: makeGridMenu(this._host),
4631
+ selection: "multi"
4632
+ }) : null;
4633
+ this._grid?.setState({
4634
+ hidden: [
4635
+ "callRho",
4636
+ "callTheta",
4637
+ "callHv",
4638
+ "callTheor",
4639
+ "putRho",
4640
+ "putTheta",
4641
+ "putHv",
4642
+ "putTheor"
4643
+ ]
4644
+ });
4645
+ this._host.register(this);
4646
+ }
4647
+ dispose() {
4648
+ this._grid?.destroy();
4649
+ this._host.unregister(this);
4650
+ try {
4651
+ this.rootEl.remove();
4652
+ } catch {
4653
+ }
4654
+ }
4655
+ /// Show this chain, priced against this context.
4656
+ ///
4657
+ /// Both together, always: a chain and the underlying it is priced off are one observation,
4658
+ /// and refreshing them separately shows greeks computed from a price that has moved.
4659
+ update(strikes, context = {}) {
4660
+ this._context = context ?? {};
4661
+ this._rows = scaleChain(strikes ?? [], this._context);
4662
+ this._places = greekScales(this._rows, this._context);
4663
+ this._grid?.setRows(this._rows);
4664
+ }
4665
+ /// The rows as the desk holds them, scales and intrinsic values resolved.
4666
+ rows() {
4667
+ return this._rows;
4668
+ }
4669
+ _export() {
4670
+ this._grid?.download("option-chain", this._host.t("OptionDesk"));
4671
+ }
4672
+ // Which side of the money this strike is on. A chain is read from the money outwards, and
4673
+ // the line between the two halves is what a reader finds first.
4674
+ _rowClass(row) {
4675
+ const asset = this._context.assetPrice;
4676
+ if (asset === null || asset === void 0) return "option-row";
4677
+ return `option-row ${row.strike < asset ? "option-itm-call" : "option-itm-put"}`;
4678
+ }
4679
+ _columns() {
4680
+ const label = (key) => this._host.t(key);
4681
+ const side = (which) => {
4682
+ const prefix = which;
4683
+ const at = (r) => r[which];
4684
+ const g = (r) => sideGreeks(r, which, this._context);
4685
+ return [
4686
+ { key: `${prefix}Rho`, header: label("Rho"), exportable: true, value: (r) => g(r)?.rho ?? null, render: (r) => decimals(g(r)?.rho, this._places.rho) },
4687
+ { key: `${prefix}Theta`, header: label("Theta"), exportable: true, value: (r) => g(r)?.theta ?? null, render: (r) => decimals(g(r)?.theta, this._places.theta) },
4688
+ { key: `${prefix}Vega`, header: label("Vega"), exportable: true, value: (r) => g(r)?.vega ?? null, render: (r) => decimals(g(r)?.vega, this._places.vega) },
4689
+ { key: `${prefix}Gamma`, header: label("Gamma"), exportable: true, value: (r) => g(r)?.gamma ?? null, render: (r) => decimals(g(r)?.gamma, this._places.gamma) },
4690
+ { key: `${prefix}Delta`, header: label("Delta"), exportable: true, value: (r) => g(r)?.delta ?? null, render: (r) => decimals(g(r)?.delta, this._places.delta) },
4691
+ { key: `${prefix}Bid`, header: label("Bid"), exportable: true, cellClass: () => "option-bid", value: (r) => at(r).bid ?? null, render: (r) => price(at(r).bid) },
4692
+ { key: `${prefix}Ask`, header: label("Ask"), exportable: true, cellClass: () => "option-ask", value: (r) => at(r).ask ?? null, render: (r) => price(at(r).ask) },
4693
+ { key: `${prefix}Theor`, header: label("TheorPrice"), exportable: true, value: (r) => at(r).theoretical ?? null, render: (r) => price(at(r).theoretical) },
4694
+ {
4695
+ key: `${prefix}Volume`,
4696
+ header: label("Volume"),
4697
+ exportable: true,
4698
+ value: (r) => at(r).volume ?? 0,
4699
+ render: (r) => this._bar(at(r).volume, which === "call" ? r.maxCallVolume : r.maxPutVolume, which, formatQty(at(r).volume ?? 0)),
4700
+ exportValue: (r) => at(r).volume ?? 0
4701
+ },
4702
+ {
4703
+ key: `${prefix}Oi`,
4704
+ header: label("OI"),
4705
+ exportable: true,
4706
+ value: (r) => at(r).openInterest ?? 0,
4707
+ render: (r) => this._bar(at(r).openInterest, which === "call" ? r.maxCallOpenInterest : r.maxPutOpenInterest, which, formatQty(at(r).openInterest ?? 0)),
4708
+ exportValue: (r) => at(r).openInterest ?? 0
4709
+ },
4710
+ { key: `${prefix}Symbol`, header: which === "call" ? label("Call") : label("Put"), exportable: true, value: (r) => at(r).symbol ?? "" },
4711
+ {
4712
+ key: `${prefix}IvBid`,
4713
+ header: label("IVBid"),
4714
+ exportable: true,
4715
+ value: (r) => at(r).ivBid ?? null,
4716
+ render: (r) => this._bar(at(r).ivBid, r.maxVolatility, "iv", percent(at(r).ivBid)),
4717
+ exportValue: (r) => at(r).ivBid ?? ""
4718
+ },
4719
+ {
4720
+ key: `${prefix}IvAsk`,
4721
+ header: label("IVAsk"),
4722
+ exportable: true,
4723
+ value: (r) => at(r).ivAsk ?? null,
4724
+ render: (r) => this._bar(at(r).ivAsk, r.maxVolatility, "iv", percent(at(r).ivAsk)),
4725
+ exportValue: (r) => at(r).ivAsk ?? ""
4726
+ },
4727
+ {
4728
+ key: `${prefix}IvLast`,
4729
+ header: label("IVLast"),
4730
+ exportable: true,
4731
+ value: (r) => at(r).ivLast ?? null,
4732
+ render: (r) => this._bar(at(r).ivLast, r.maxVolatility, "iv", percent(at(r).ivLast)),
4733
+ exportValue: (r) => at(r).ivLast ?? ""
4734
+ },
4735
+ {
4736
+ key: `${prefix}Hv`,
4737
+ header: label("HV"),
4738
+ exportable: true,
4739
+ value: (r) => at(r).historicalVolatility ?? null,
4740
+ render: (r) => this._bar(at(r).historicalVolatility, r.maxVolatility, "iv", percent(at(r).historicalVolatility)),
4741
+ exportValue: (r) => at(r).historicalVolatility ?? ""
4742
+ }
4743
+ ];
4744
+ };
4745
+ const puts = side("put").slice().reverse();
4746
+ return [
4747
+ ...side("call"),
4748
+ {
4749
+ key: "strike",
4750
+ header: label("Strike"),
4751
+ exportable: true,
4752
+ cellClass: () => "option-strike",
4753
+ value: (r) => r.strike,
4754
+ render: (r) => price(r.strike)
4755
+ },
4756
+ {
4757
+ key: "intrinsic",
4758
+ header: label("IntrinsicValue"),
4759
+ exportable: true,
4760
+ cellClass: () => "option-intrinsic",
4761
+ // Whichever side is in the money at this strike; the other is worth nothing to
4762
+ // exercise, and showing both would be one number and one zero on every row.
4763
+ value: (r) => Math.max(r.callIntrinsic, r.putIntrinsic),
4764
+ render: (r) => price(Math.max(r.callIntrinsic, r.putIntrinsic))
4765
+ },
4766
+ ...puts
4767
+ ];
4768
+ }
4769
+ // A figure with its share of the strip behind it. The bar is a width, not a drawing: it
4770
+ // scales with the cell, it prints, and it needs no canvas per row.
4771
+ _bar(value, max, kind, text) {
4772
+ if (value === null || value === void 0 || !isFinite(value) || value <= 0) return text;
4773
+ const cell = document.createElement("span");
4774
+ cell.className = "option-bar-cell";
4775
+ const bar = document.createElement("span");
4776
+ bar.className = `option-bar option-bar-${kind}`;
4777
+ bar.style.width = `${Math.min(100, value / (max || 1) * 100).toFixed(1)}%`;
4778
+ cell.appendChild(bar);
4779
+ const label = document.createElement("span");
4780
+ label.className = "option-bar-text";
4781
+ label.textContent = text;
4782
+ cell.appendChild(label);
4783
+ return cell;
4784
+ }
4785
+ };
4786
+ _OptionDeskWidget.TYPE = ControlTypes.OptionDesk;
4787
+ var OptionDeskWidget = _OptionDeskWidget;
4788
+ function scaleChain(strikes, context) {
4789
+ const asset = context.assetPrice ?? null;
4790
+ const maxOf = (pick, sides) => Math.max(0, ...strikes.flatMap((r) => sides(r).map((s) => pick(s) ?? 0)).filter((v) => isFinite(v)));
4791
+ const maxCallVolume = maxOf((s) => s.volume, (r) => [r.call]);
4792
+ const maxPutVolume = maxOf((s) => s.volume, (r) => [r.put]);
4793
+ const maxCallOpenInterest = maxOf((s) => s.openInterest, (r) => [r.call]);
4794
+ const maxPutOpenInterest = maxOf((s) => s.openInterest, (r) => [r.put]);
4795
+ const maxVolatility = Math.max(
4796
+ maxOf((s) => s.ivBid, (r) => [r.call, r.put]),
4797
+ maxOf((s) => s.ivAsk, (r) => [r.call, r.put]),
4798
+ maxOf((s) => s.ivLast, (r) => [r.call, r.put]),
4799
+ maxOf((s) => s.historicalVolatility, (r) => [r.call, r.put])
4800
+ );
4801
+ return strikes.map((row) => ({
4802
+ ...row,
4803
+ maxCallVolume,
4804
+ maxPutVolume,
4805
+ maxCallOpenInterest,
4806
+ maxPutOpenInterest,
4807
+ maxVolatility,
4808
+ callIntrinsic: asset === null ? 0 : Math.max(0, asset - row.strike),
4809
+ putIntrinsic: asset === null ? 0 : Math.max(0, row.strike - asset)
4810
+ }));
4811
+ }
4812
+ function sideGreeks(row, which, context) {
4813
+ const side = row[which];
4814
+ if (side.greeks !== void 0) return side.greeks;
4815
+ const deviation = side.ivLast ?? side.ivBid ?? side.ivAsk ?? side.historicalVolatility ?? null;
4816
+ const assetPrice = context.assetPrice ?? null;
4817
+ const timeToExpiry = context.timeToExpiry ?? null;
4818
+ if (deviation === null || assetPrice === null || timeToExpiry === null) return null;
4819
+ return greeks(which === "call" ? OptionTypes.Call : OptionTypes.Put, {
4820
+ assetPrice,
4821
+ strike: row.strike,
4822
+ timeToExpiry,
4823
+ riskFree: context.riskFree ?? 0,
4824
+ dividend: context.dividend ?? 0,
4825
+ deviation
4826
+ });
4827
+ }
4828
+ var GREEK_KEYS = ["delta", "gamma", "vega", "theta", "rho"];
4829
+ var GREEK_DIGITS = 4;
4830
+ var GREEK_MIN_PLACES = 2;
4831
+ var GREEK_MAX_PLACES = 8;
4832
+ function greekPlaces(values) {
4833
+ const scale = Math.min(...values.filter((v) => typeof v === "number" && isFinite(v) && v !== 0).map(Math.abs));
4834
+ if (!isFinite(scale)) return GREEK_MIN_PLACES;
4835
+ const places = GREEK_DIGITS - 1 - Math.floor(Math.log10(scale));
4836
+ return Math.min(GREEK_MAX_PLACES, Math.max(GREEK_MIN_PLACES, places));
4837
+ }
4838
+ function greekScales(rows, context) {
4839
+ const all = rows.flatMap((r) => [sideGreeks(r, OptionTypes.Call, context), sideGreeks(r, OptionTypes.Put, context)]).filter((g) => g !== null);
4840
+ const places = {};
4841
+ for (const key of GREEK_KEYS) places[key] = greekPlaces(all.map((g) => g[key]));
4842
+ return places;
4843
+ }
4844
+ function decimals(value, places) {
4845
+ if (value === null || value === void 0 || !isFinite(value)) return "";
4846
+ return value.toFixed(places);
4847
+ }
4848
+ function price(value) {
4849
+ if (value === null || value === void 0 || !isFinite(value)) return "";
4850
+ return formatPrice(value);
4851
+ }
4852
+ function percent(value) {
4853
+ if (value === null || value === void 0 || !isFinite(value)) return "";
4854
+ return `${(value * 100).toFixed(2)}%`;
4855
+ }
4856
+
4857
+ // src/strategies-widget.ts
4858
+ var StrategyStates = {
4859
+ Stopped: "stopped",
4860
+ Starting: "starting",
4861
+ Started: "started",
4862
+ Stopping: "stopping"
4863
+ };
4864
+ var SPARK_WIDTH = 140;
4865
+ var SPARK_HEIGHT = 26;
4866
+ var _StrategiesWidget = class _StrategiesWidget {
4867
+ static create(hostEl, state, deps) {
4868
+ const host = assertHost(deps?.host, "StrategiesWidget");
4869
+ const root = _StrategiesWidget._buildRoot(host);
4870
+ root.id = makePanelId(_StrategiesWidget.TYPE);
4871
+ hostEl.appendChild(root);
4872
+ return new _StrategiesWidget(root, state || {}, deps);
4873
+ }
4874
+ static _buildRoot(host) {
4875
+ const title = host.t("Strategies");
4876
+ return makePanelRoot("strategies-panel", title, [
4877
+ makeElement("div", "panel-header", {}, [
4878
+ makeElement("span", "", {}, [title]),
4879
+ makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
4880
+ ]),
4881
+ makeElement("div", "panel-body panel-body-with-rail", {}, [
4882
+ makeElement("div", "panel-body-content", {}, [
4883
+ makeElement("table", "terminal-table strategies-table", { role: "table", "aria-label": host.t("StrategiesList") }, [
4884
+ makeElement("thead", "", {}, []),
4885
+ makeElement("tbody", "strategies-body", {}, [])
4886
+ ])
4887
+ ]),
4888
+ makeElement("div", "panel-rail", { role: "toolbar", "aria-label": host.t("StrategiesActions") }, [
4889
+ makeIconButton("bt-icon-btn panel-export-btn", host.t("ExportToExcel"), "bi-file-earmark-spreadsheet", {})
4890
+ ])
4891
+ ])
4892
+ ]);
4893
+ }
4894
+ constructor(rootEl, _state, deps) {
4895
+ this._host = assertHost(deps?.host, "StrategiesWidget");
4896
+ this._deps = deps;
4897
+ this.rootEl = rootEl;
4898
+ this.el = this.rootEl.querySelector(".strategies-body");
4899
+ this._closeBtn = this.rootEl.querySelector(".panel-close-btn");
4900
+ this._exportBtn = this.rootEl.querySelector(".panel-export-btn");
4901
+ this._rows = [];
4902
+ this._closeBtn?.addEventListener("click", (e) => {
4903
+ e.preventDefault();
4904
+ this._host.close();
4905
+ });
4906
+ this._exportBtn?.addEventListener("click", (e) => {
4907
+ e.preventDefault();
4908
+ this._export();
4909
+ });
4910
+ const head = this.rootEl.querySelector(".strategies-table thead");
4911
+ this._grid = head && this.el ? new DataGrid({
4912
+ head,
4913
+ body: this.el,
4914
+ columns: this._columns(),
4915
+ // By name: a dashboard is a list someone reads down looking for one strategy,
4916
+ // and a list that reorders itself as P&L moves cannot be read that way.
4917
+ defaultSort: { col: "name", dir: "asc" },
4918
+ rowKey: (s) => String(s.id),
4919
+ emptyText: this._host.t("NoStrategies"),
4920
+ rowClass: (s) => `strategy-row strategy-${s.state}${s.error ? " strategy-failed" : ""}`,
4921
+ contextMenu: makeGridMenu(this._host),
4922
+ selection: "multi"
4923
+ }) : null;
4924
+ this._host.register(this);
4925
+ }
4926
+ dispose() {
4927
+ this._grid?.destroy();
4928
+ this._host.unregister(this);
4929
+ try {
4930
+ this.rootEl.remove();
4931
+ } catch {
4932
+ }
4933
+ }
4934
+ /// Show these strategies. The whole set: one that is gone from it has been removed, and
4935
+ /// leaving its row behind would offer a Stop for something that no longer runs.
4936
+ update(rows) {
4937
+ this._rows = rows || [];
4938
+ this._grid?.setRows(this._rows);
4939
+ }
4940
+ _export() {
4941
+ this._grid?.download("strategies", this._host.t("Strategies"));
4942
+ }
4943
+ _columns() {
4944
+ const label = (key) => this._host.t(key);
4945
+ const presentation = this._host.presentation;
4946
+ return [
4947
+ {
4948
+ key: "state",
4949
+ header: label("State"),
4950
+ exportable: true,
4951
+ value: (s) => s.state,
4952
+ render: (s) => this._stateCell(s),
4953
+ cellClass: (s) => `strategy-state strategy-state-${s.state}`,
4954
+ exportValue: (s) => s.state
4955
+ },
4956
+ {
4957
+ key: "actions",
4958
+ header: label("Actions"),
4959
+ headerHidden: true,
4960
+ exportable: false,
4961
+ cellClass: () => "strategy-actions",
4962
+ render: (s) => this._actionCell(s)
4963
+ },
4964
+ {
4965
+ key: "online",
4966
+ header: label("Online"),
4967
+ exportable: true,
4968
+ value: (s) => s.online ? 1 : 0,
4969
+ render: (s) => s.online ? "\u25CF" : "\u25CB",
4970
+ cellClass: (s) => s.online ? "strategy-online is-on" : "strategy-online",
4971
+ exportValue: (s) => s.online ? label("Yes") : label("No")
4972
+ },
4973
+ {
4974
+ key: "tradingMode",
4975
+ header: label("Trading"),
4976
+ exportable: true,
4977
+ value: (s) => s.tradingMode ?? "",
4978
+ render: (s) => this._tradingCell(s),
4979
+ exportValue: (s) => s.tradingMode ?? ""
4980
+ },
4981
+ { key: "name", header: label("Name"), exportable: true, value: (s) => s.name },
4982
+ { key: "portfolio", header: label("Portfolio"), exportable: true, value: (s) => s.portfolio ?? "" },
4983
+ { key: "security", header: label("Sym"), exportable: true, value: (s) => s.security ?? "" },
4984
+ {
4985
+ key: "position",
4986
+ header: label("Position"),
4987
+ exportable: true,
4988
+ value: (s) => s.position ?? 0,
4989
+ render: (s) => this._positionCell(s),
4990
+ cellClass: (s) => `strategy-position ${presentation.pnlClass(s.position ?? 0)}`,
4991
+ exportValue: (s) => s.position ?? 0
4992
+ },
4993
+ { key: "orders", header: label("OrderCount"), exportable: true, value: (s) => s.ordersCount ?? 0 },
4994
+ { key: "trades", header: label("NumOfTrades"), exportable: true, value: (s) => s.tradesCount ?? 0 },
4995
+ {
4996
+ key: "pnlChange",
4997
+ header: label("PnLChange"),
4998
+ exportable: true,
4999
+ value: (s) => s.pnlChange ?? 0,
5000
+ render: (s) => `${direction(s.pnlChange ?? 0)} ${formatPnl(s.pnlChange ?? 0)}`,
5001
+ cellClass: (s) => presentation.pnlClass(s.pnlChange ?? 0),
5002
+ exportValue: (s) => s.pnlChange ?? 0
5003
+ },
5004
+ {
5005
+ key: "pnlChart",
5006
+ header: label("PnLChart"),
5007
+ exportable: false,
5008
+ cellClass: () => "strategy-spark",
5009
+ render: (s) => this._sparkline(s)
5010
+ },
5011
+ {
5012
+ key: "realized",
5013
+ header: label("RealizedProfit"),
5014
+ exportable: true,
5015
+ value: (s) => s.realized ?? 0,
5016
+ render: (s) => formatPnl(s.realized ?? 0),
5017
+ cellClass: (s) => presentation.pnlClass(s.realized ?? 0),
5018
+ exportValue: (s) => s.realized ?? 0
5019
+ },
5020
+ {
5021
+ key: "unrealized",
5022
+ header: label("UnrealizedProfit"),
5023
+ exportable: true,
5024
+ value: (s) => s.unrealized ?? 0,
5025
+ render: (s) => formatPnl(s.unrealized ?? 0),
5026
+ cellClass: (s) => presentation.pnlClass(s.unrealized ?? 0),
5027
+ exportValue: (s) => s.unrealized ?? 0
5028
+ },
5029
+ {
5030
+ key: "error",
5031
+ header: label("Error"),
5032
+ exportable: true,
5033
+ cellClass: () => "strategy-error",
5034
+ value: (s) => s.error ?? ""
5035
+ }
5036
+ ];
5037
+ }
5038
+ // A dot and the state's own word. The dot is what is read across a list of twenty; the word
5039
+ // is what tells Starting from Started, which a colour alone cannot.
5040
+ _stateCell(row) {
5041
+ const cell = document.createDocumentFragment();
5042
+ const failed = row.error !== void 0 && row.error !== null && String(row.error).length > 0;
5043
+ const dot = document.createElement("span");
5044
+ dot.className = "strategy-dot";
5045
+ dot.textContent = "\u25CF";
5046
+ if (failed) dot.title = String(row.error);
5047
+ cell.appendChild(dot);
5048
+ const text = document.createElement("span");
5049
+ text.className = "strategy-state-text";
5050
+ text.textContent = failed && row.state === StrategyStates.Stopped ? this._host.t("Error") : stateText(this._host, row.state);
5051
+ if (failed) text.title = String(row.error);
5052
+ cell.appendChild(text);
5053
+ return cell;
5054
+ }
5055
+ // Only the buttons this host can actually carry out, and only where the state allows them.
5056
+ // A strategy that is already running has nothing to start.
5057
+ _actionCell(row) {
5058
+ const cell = document.createDocumentFragment();
5059
+ const deps = this._deps;
5060
+ const add = (action, when, title, icon, cls) => {
5061
+ if (action === void 0) return;
5062
+ const button = makeIconButton(`bt-icon-btn ${cls}`, title, icon, { type: "button" });
5063
+ if (!when) button.disabled = true;
5064
+ else button.addEventListener("click", () => action.call(deps, row.id));
5065
+ cell.appendChild(button);
5066
+ };
5067
+ add(deps.start, row.state === StrategyStates.Stopped, this._host.t("Start"), "bi-play-fill", "strategy-start-btn");
5068
+ add(deps.stop, row.state === StrategyStates.Started, this._host.t("Stop"), "bi-stop-fill", "strategy-stop-btn");
5069
+ add(deps.riskRules, true, this._host.t("RiskManagement"), "bi-shield-exclamation", "strategy-risk-btn");
5070
+ add(deps.openStrategy, true, this._host.t("OpenStrategy"), "bi-box-arrow-up-right", "strategy-open-btn");
5071
+ return cell;
5072
+ }
5073
+ // The position, and next to it the one gesture that acts on it. Flattening is only
5074
+ // meaningful while the strategy runs and only when it holds something.
5075
+ _positionCell(row) {
5076
+ const position = row.position ?? 0;
5077
+ const text = formatQty(position);
5078
+ if (this._deps.closePosition === void 0) return text;
5079
+ const cell = document.createDocumentFragment();
5080
+ const value = makeElement("span", "strategy-position-value", {}, [text]);
5081
+ cell.appendChild(value);
5082
+ const button = makeIconButton("bt-icon-btn strategy-flatten-btn", this._host.t("ClosePosition"), "bi-x-octagon", { type: "button" });
5083
+ if (row.state !== StrategyStates.Started || position === 0) button.disabled = true;
5084
+ else button.addEventListener("click", () => this._deps.closePosition?.(row.id));
5085
+ cell.appendChild(button);
5086
+ return cell;
5087
+ }
5088
+ // The one editable cell, and only when the host can carry the change out.
5089
+ _tradingCell(row) {
5090
+ const modes = this._deps.tradingModes ?? [];
5091
+ if (this._deps.setTradingMode === void 0 || modes.length === 0) return row.tradingMode ?? "";
5092
+ const select = document.createElement("select");
5093
+ select.className = "form-control form-control-sm strategy-mode";
5094
+ select.disabled = row.state !== StrategyStates.Stopped;
5095
+ for (const mode of modes) {
5096
+ const option = document.createElement("option");
5097
+ option.value = mode;
5098
+ option.textContent = this._host.t(mode);
5099
+ if (mode === row.tradingMode) option.selected = true;
5100
+ select.appendChild(option);
5101
+ }
5102
+ if (!select.disabled)
5103
+ select.addEventListener("change", () => this._deps.setTradingMode?.(row.id, select.value));
5104
+ return select;
5105
+ }
5106
+ // The run's own curve, at the size a cell has. Coloured by where it ended, which is the
5107
+ // whole reason a glance at the column tells one strategy from another.
5108
+ _sparkline(row) {
5109
+ const box = { width: SPARK_WIDTH, height: SPARK_HEIGHT, padX: 1, padY: 2 };
5110
+ const curve = pnlCurve(row.pnl ?? [], box);
5111
+ if (curve === null) return "";
5112
+ const canvas = document.createElement("canvas");
5113
+ canvas.className = "strategy-spark-canvas";
5114
+ const ratio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
5115
+ canvas.width = Math.round(box.width * ratio);
5116
+ canvas.height = Math.round(box.height * ratio);
5117
+ canvas.style.width = `${box.width}px`;
5118
+ canvas.style.height = `${box.height}px`;
5119
+ const ctx = canvas.getContext("2d");
5120
+ if (ctx !== null) {
5121
+ const palette = this._host.presentation.canvasPalette();
5122
+ const scaled = { width: canvas.width, height: canvas.height, padX: box.padX * ratio, padY: box.padY * ratio };
5123
+ drawPnlCurve(ctx, pnlCurve(row.pnl ?? [], scaled) ?? curve, scaled, {
5124
+ up: palette.up,
5125
+ down: palette.down,
5126
+ baseline: palette.grid,
5127
+ lineWidth: 1.5 * ratio,
5128
+ fillOpacity: 0.35
5129
+ });
5130
+ }
5131
+ return canvas;
5132
+ }
5133
+ };
5134
+ _StrategiesWidget.TYPE = ControlTypes.Strategies;
5135
+ var StrategiesWidget = _StrategiesWidget;
5136
+ function stateText(host, state) {
5137
+ switch (state) {
5138
+ case StrategyStates.Started:
5139
+ return host.t("Started");
5140
+ case StrategyStates.Starting:
5141
+ return host.t("Starting");
5142
+ case StrategyStates.Stopping:
5143
+ return host.t("Stopping");
5144
+ default:
5145
+ return host.t("Stopped");
5146
+ }
5147
+ }
5148
+ function direction(value) {
5149
+ if (value > 0) return "\u25B2";
5150
+ if (value < 0) return "\u25BC";
5151
+ return "";
5152
+ }
5153
+
5154
+ // src/log-tree.ts
5155
+ var LogLevels = {
5156
+ Error: "error",
5157
+ Warning: "warning",
5158
+ Info: "info",
5159
+ Debug: "debug",
5160
+ Verbose: "verbose"
5161
+ };
5162
+ function buildLogTree(sources) {
5163
+ const byId = /* @__PURE__ */ new Map();
5164
+ for (const s of sources)
5165
+ byId.set(s.id, { ...s, name: s.name && s.name.length > 0 ? s.name : "\u2014", depth: 0, children: [] });
5166
+ const roots = [];
5167
+ for (const node of byId.values()) {
5168
+ const parent = node.parentId != null ? byId.get(node.parentId) : void 0;
5169
+ if (parent !== void 0 && parent !== node && !descends(byId, parent, node.id)) parent.children.push(node);
5170
+ else roots.push(node);
5171
+ }
5172
+ const setDepth = (node, depth) => {
5173
+ node.depth = depth;
5174
+ for (const child of node.children) setDepth(child, depth + 1);
5175
+ };
5176
+ for (const root of roots) setDepth(root, 0);
5177
+ return roots;
5178
+ }
5179
+ function descends(byId, node, id) {
5180
+ const seen = /* @__PURE__ */ new Set();
5181
+ let at = node;
5182
+ while (at !== void 0 && !seen.has(at.id)) {
5183
+ if (at.id === id) return true;
5184
+ seen.add(at.id);
5185
+ at = at.parentId != null ? byId.get(at.parentId) : void 0;
5186
+ }
5187
+ return false;
5188
+ }
5189
+ function subtreeOf(sources, selectedId) {
5190
+ if (selectedId === null) return null;
5191
+ const children = /* @__PURE__ */ new Map();
5192
+ for (const s of sources) {
5193
+ if (s.parentId == null) continue;
5194
+ const siblings = children.get(s.parentId);
5195
+ if (siblings === void 0) children.set(s.parentId, [s.id]);
5196
+ else siblings.push(s.id);
5197
+ }
5198
+ const out = /* @__PURE__ */ new Set();
5199
+ const walk = (id) => {
5200
+ if (out.has(id)) return;
5201
+ out.add(id);
5202
+ for (const child of children.get(id) ?? []) walk(child);
5203
+ };
5204
+ walk(selectedId);
5205
+ return out;
5206
+ }
5207
+ function keepLog(message, view) {
5208
+ if (!view.levels.has(message.level)) return false;
5209
+ if (view.sources !== null && !view.sources.has(message.sourceId)) return false;
5210
+ const text = view.text.trim();
5211
+ if (text.length === 0) return true;
5212
+ return (message.message ?? "").toLowerCase().includes(text.toLowerCase());
5213
+ }
5214
+
5215
+ // src/log-monitor-widget.ts
5216
+ var DEFAULT_MAX = 5e3;
5217
+ var LEVEL_LETTER = {
5218
+ [LogLevels.Error]: "E",
5219
+ [LogLevels.Warning]: "W",
5220
+ [LogLevels.Info]: "I",
5221
+ [LogLevels.Debug]: "D",
5222
+ [LogLevels.Verbose]: "V"
5223
+ };
5224
+ var ALL_LEVELS = [LogLevels.Error, LogLevels.Warning, LogLevels.Info, LogLevels.Debug, LogLevels.Verbose];
5225
+ var _LogMonitorWidget = class _LogMonitorWidget {
5226
+ static create(hostEl, state, deps) {
5227
+ const host = assertHost(deps?.host, "LogMonitorWidget");
5228
+ const root = _LogMonitorWidget._buildRoot(host);
5229
+ root.id = makePanelId(_LogMonitorWidget.TYPE);
5230
+ hostEl.appendChild(root);
5231
+ return new _LogMonitorWidget(root, state || {}, deps);
5232
+ }
5233
+ // The panel's markup: a source tree, a toolbar of level toggles and a text box, and the
5234
+ // message table. The level toggles are buttons rather than checkboxes because they are read
5235
+ // as a row of states, and each carries its letter so the toolbar and the column agree.
5236
+ static _buildRoot(host) {
5237
+ const title = host.t("LogMonitor");
5238
+ return makePanelRoot("log-monitor-panel", title, [
5239
+ makeElement("div", "panel-header", {}, [
5240
+ makeElement("span", "", {}, [title]),
5241
+ makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
5242
+ ]),
5243
+ makeElement("div", "panel-body log-monitor-body", {}, [
5244
+ makeElement("div", "log-sources", { role: "tree", "aria-label": host.t("LogSources") }, []),
5245
+ makeElement("div", "log-messages", {}, [
5246
+ makeElement("div", "log-toolbar", { role: "toolbar", "aria-label": host.t("LogMonitorActions") }, [
5247
+ ...ALL_LEVELS.map((level) => makeElement("button", `log-level-toggle log-level-${level} is-on`, {
5248
+ type: "button",
5249
+ "data-level": level,
5250
+ "aria-pressed": "true",
5251
+ title: levelTitle(host, level)
5252
+ }, [LEVEL_LETTER[level]])),
5253
+ makeElement("input", "form-control form-control-sm log-filter", {
5254
+ type: "search",
5255
+ placeholder: host.t("Filter"),
5256
+ "aria-label": host.t("Filter")
5257
+ }, []),
5258
+ makeIconButton("bt-icon-btn log-clear-btn", host.t("ClearItems"), "bi-eraser", {}),
5259
+ makeIconButton("bt-icon-btn panel-export-btn", host.t("ExportToExcel"), "bi-file-earmark-spreadsheet", {})
5260
+ ]),
5261
+ // The table sits in a box of its own rather than being a flex child: a table
5262
+ // told to fill a column stretches its rows to do it, so a log holding one
5263
+ // line drew that line a panel tall - and with the toolbar and the table as
5264
+ // the only two children there was nowhere for a long log to scroll.
5265
+ makeElement("div", "log-table-scroll", {}, [
5266
+ makeElement("table", "terminal-table log-table", { role: "table", "aria-label": host.t("LogMessages") }, [
5267
+ makeElement("thead", "", {}, []),
5268
+ makeElement("tbody", "log-body", {}, [])
5269
+ ])
5270
+ ])
5271
+ ])
5272
+ ])
5273
+ ]);
5274
+ }
5275
+ constructor(rootEl, _state, deps) {
5276
+ this._host = assertHost(deps?.host, "LogMonitorWidget");
5277
+ this._max = deps?.maxMessages && deps.maxMessages > 0 ? deps.maxMessages : DEFAULT_MAX;
5278
+ this.rootEl = rootEl;
5279
+ this.el = this.rootEl.querySelector(".log-body");
5280
+ this._treeEl = this.rootEl.querySelector(".log-sources");
5281
+ this._filterEl = this.rootEl.querySelector(".log-filter");
5282
+ this._closeBtn = this.rootEl.querySelector(".panel-close-btn");
5283
+ this._clearBtn = this.rootEl.querySelector(".log-clear-btn");
5284
+ this._exportBtn = this.rootEl.querySelector(".panel-export-btn");
5285
+ this._sources = [];
5286
+ this._messages = [];
5287
+ this._levels = new Set(ALL_LEVELS);
5288
+ this._text = "";
5289
+ this._selected = null;
5290
+ this._closeBtn?.addEventListener("click", (e) => {
5291
+ e.preventDefault();
5292
+ this._host.close();
5293
+ });
5294
+ this._clearBtn?.addEventListener("click", (e) => {
5295
+ e.preventDefault();
5296
+ this.clear();
5297
+ });
5298
+ this._exportBtn?.addEventListener("click", (e) => {
5299
+ e.preventDefault();
5300
+ this._export();
5301
+ });
5302
+ for (const toggle of Array.from(this.rootEl.querySelectorAll(".log-level-toggle"))) {
5303
+ toggle.addEventListener("click", (e) => {
5304
+ e.preventDefault();
5305
+ const level = toggle.getAttribute("data-level") ?? "";
5306
+ if (this._levels.has(level)) this._levels.delete(level);
5307
+ else this._levels.add(level);
5308
+ toggle.classList.toggle("is-on", this._levels.has(level));
5309
+ toggle.setAttribute("aria-pressed", this._levels.has(level) ? "true" : "false");
5310
+ this._render();
5311
+ });
5312
+ }
5313
+ this._filterEl?.addEventListener("input", () => {
5314
+ this._text = this._filterEl?.value ?? "";
5315
+ this._render();
5316
+ });
5317
+ const head = this.rootEl.querySelector(".log-table thead");
5318
+ this._grid = head && this.el ? new DataGrid({
5319
+ head,
5320
+ body: this.el,
5321
+ columns: this._columns(),
5322
+ // The order it happened in. A log read out of order is not a log.
5323
+ defaultSort: { col: "time", dir: "asc" },
5324
+ rowKey: (m) => String(m.id),
5325
+ emptyText: this._host.t("NoLogMessages"),
5326
+ rowClass: (m) => `log-row log-row-${m.level}`,
5327
+ contextMenu: makeGridMenu(this._host),
5328
+ selection: "multi"
5329
+ }) : null;
5330
+ this._host.register(this);
5331
+ }
5332
+ dispose() {
5333
+ this._grid?.destroy();
5334
+ this._host.unregister(this);
5335
+ try {
5336
+ this.rootEl.remove();
5337
+ } catch {
5338
+ }
5339
+ }
5340
+ /// The sources that exist. Sent whole: a source that has gone is gone from the tree, and
5341
+ /// the selection falls back to everything rather than to a node nobody can see.
5342
+ setSources(sources) {
5343
+ this._sources = sources || [];
5344
+ if (this._selected !== null && !this._sources.some((s) => s.id === this._selected)) this._selected = null;
5345
+ this._renderTree();
5346
+ this._render();
5347
+ }
5348
+ /// Add what has just been written. The oldest fall off the front once the cap is reached.
5349
+ append(messages) {
5350
+ if (!messages || messages.length === 0) return;
5351
+ this._messages.push(...messages);
5352
+ if (this._messages.length > this._max) this._messages.splice(0, this._messages.length - this._max);
5353
+ this._render();
5354
+ }
5355
+ /// Forget every message. The sources stay: they still exist, they have just gone quiet.
5356
+ clear() {
5357
+ this._messages = [];
5358
+ this._render();
5359
+ }
5360
+ /// Show one source's subtree, or everything when given null.
5361
+ select(sourceId) {
5362
+ this._selected = sourceId;
5363
+ this._renderTree();
5364
+ this._render();
5365
+ }
5366
+ /// What is on screen, after every filter.
5367
+ visible() {
5368
+ const sources = subtreeOf(this._sources, this._selected);
5369
+ return this._messages.filter((m) => keepLog(m, { levels: this._levels, text: this._text, sources }));
5370
+ }
5371
+ _render() {
5372
+ this._grid?.setRows(this.visible());
5373
+ }
5374
+ _renderTree() {
5375
+ const tree = this._treeEl;
5376
+ if (tree === null) return;
5377
+ tree.innerHTML = "";
5378
+ tree.appendChild(this._treeRow({ id: "", name: this._host.t("AllSources"), depth: 0, children: [] }, null));
5379
+ for (const root of buildLogTree(this._sources)) this._appendNode(tree, root);
5380
+ }
5381
+ _appendNode(into, node) {
5382
+ into.appendChild(this._treeRow(node, node.id));
5383
+ for (const child of node.children) this._appendNode(into, child);
5384
+ }
5385
+ // Flat rows with an indent rather than nested lists: the tree is read, not restructured,
5386
+ // and one list is what a keyboard walks through in the order the eye does.
5387
+ _treeRow(node, id) {
5388
+ const selected = this._selected === id;
5389
+ const row = makeElement("button", `log-source${selected ? " is-selected" : ""}`, {
5390
+ type: "button",
5391
+ role: "treeitem",
5392
+ "aria-selected": selected ? "true" : "false",
5393
+ "aria-level": String(node.depth + 1),
5394
+ style: `padding-left: ${0.5 + node.depth * 0.85}rem`
5395
+ }, [node.name]);
5396
+ row.addEventListener("click", (e) => {
5397
+ e.preventDefault();
5398
+ this.select(id);
5399
+ });
5400
+ return row;
5401
+ }
5402
+ _export() {
5403
+ this._grid?.download("log", this._host.t("LogMonitor"));
5404
+ }
5405
+ _columns() {
5406
+ const label = (key) => this._host.t(key);
5407
+ return [
5408
+ {
5409
+ key: "source",
5410
+ header: label("Source"),
5411
+ exportable: true,
5412
+ value: (m) => m.source ?? this._sourceName(m.sourceId)
5413
+ },
5414
+ {
5415
+ key: "time",
5416
+ header: label("Time"),
5417
+ exportable: true,
5418
+ value: (m) => m.time,
5419
+ render: (m) => this._host.presentation.timeText(m.time),
5420
+ exportValue: (m) => this._host.presentation.timeText(m.time)
5421
+ },
5422
+ {
5423
+ key: "level",
5424
+ header: label("Type"),
5425
+ headerClass: "log-level-col",
5426
+ exportable: true,
5427
+ cellClass: (m) => `log-level-cell log-level-${m.level}`,
5428
+ value: (m) => m.level,
5429
+ render: (m) => LEVEL_LETTER[m.level] ?? String(m.level ?? "").slice(0, 1).toUpperCase(),
5430
+ // The sheet carries the level's name, not the letter the column has room for.
5431
+ exportValue: (m) => m.level
5432
+ },
5433
+ {
5434
+ key: "message",
5435
+ header: label("Message"),
5436
+ exportable: true,
5437
+ cellClass: () => "log-message-cell",
5438
+ value: (m) => m.message
5439
+ }
5440
+ ];
5441
+ }
5442
+ _sourceName(id) {
5443
+ return this._sources.find((s) => s.id === id)?.name ?? id;
5444
+ }
5445
+ };
5446
+ _LogMonitorWidget.TYPE = ControlTypes.LogMonitor;
5447
+ var LogMonitorWidget = _LogMonitorWidget;
5448
+ function levelTitle(host, level) {
5449
+ switch (level) {
5450
+ case LogLevels.Error:
5451
+ return host.t("Errors");
5452
+ case LogLevels.Warning:
5453
+ return host.t("Warnings");
5454
+ case LogLevels.Info:
5455
+ return host.t("Messages");
5456
+ case LogLevels.Debug:
5457
+ return host.t("Debug");
5458
+ default:
5459
+ return host.t("Verbose");
5460
+ }
5461
+ }
5462
+
5463
+ // src/statistics-widget.ts
5464
+ function rank(rows) {
5465
+ const first = /* @__PURE__ */ new Map();
5466
+ for (const row of rows) {
5467
+ const seen = first.get(row.category);
5468
+ if (seen === void 0 || row.order < seen) first.set(row.category, row.order);
5469
+ }
5470
+ return rows.map((row) => ({ ...row, categoryRank: first.get(row.category) ?? row.order }));
5471
+ }
5472
+ var _StatisticsWidget = class _StatisticsWidget {
5473
+ static create(hostEl, state, deps) {
5474
+ const host = assertHost(deps?.host, "StatisticsWidget");
5475
+ const root = _StatisticsWidget._buildRoot(host);
5476
+ root.id = makePanelId(_StatisticsWidget.TYPE);
5477
+ hostEl.appendChild(root);
5478
+ return new _StatisticsWidget(root, state || {}, deps);
5479
+ }
5480
+ // The panel's markup. No rail action but the export: a statistic is produced by the
5481
+ // strategy and there is nothing here to cancel, reload or edit.
5482
+ static _buildRoot(host) {
5483
+ const title = host.t("Statistics");
5484
+ return makePanelRoot("statistics-panel", title, [
5485
+ makeElement("div", "panel-header", {}, [
5486
+ makeElement("span", "", {}, [title]),
5487
+ makeIconButton("bt-icon-btn bt-icon-cancel panel-close-btn", host.t("ClosePanel"), "bi-x", { type: "button" })
5488
+ ]),
5489
+ makeElement("div", "panel-body panel-body-with-rail", {}, [
5490
+ makeElement("div", "panel-body-content", {}, [
5491
+ makeElement("table", "terminal-table statistics-table", { role: "table", "aria-label": host.t("StatisticsList") }, [
5492
+ makeElement("thead", "", {}, []),
5493
+ makeElement("tbody", "statistics-body", {}, [])
5494
+ ])
5495
+ ]),
5496
+ makeElement("div", "panel-rail", { role: "toolbar", "aria-label": host.t("StatisticsActions") }, [
5497
+ makeIconButton("bt-icon-btn panel-export-btn", host.t("ExportToExcel"), "bi-file-earmark-spreadsheet", {})
5498
+ ])
5499
+ ])
5500
+ ]);
5501
+ }
5502
+ constructor(rootEl, _state, deps) {
5503
+ this._host = assertHost(deps?.host, "StatisticsWidget");
5504
+ this.rootEl = rootEl;
5505
+ this.el = this.rootEl.querySelector(".statistics-body");
5506
+ this._closeBtn = this.rootEl.querySelector(".panel-close-btn");
5507
+ this._exportBtn = this.rootEl.querySelector(".panel-export-btn");
5508
+ this._rows = [];
5509
+ this._closeBtn?.addEventListener("click", (e) => {
5510
+ e.preventDefault();
5511
+ this._host.close();
5512
+ });
5513
+ this._exportBtn?.addEventListener("click", (e) => {
5514
+ e.preventDefault();
5515
+ this._export();
5516
+ });
5517
+ const head = this.rootEl.querySelector(".statistics-table thead");
5518
+ this._grid = head && this.el ? new DataGrid({
5519
+ head,
5520
+ body: this.el,
5521
+ columns: this._columns(),
5522
+ // Not a sort the reader chose: the registry hands every parameter an order, and
5523
+ // the bands it assigns are what puts profit above drawdown above trade counts.
5524
+ defaultSort: { col: "order", dir: "asc" },
5525
+ rowKey: (r) => String(r.key),
5526
+ emptyText: this._host.t("NoStatistics"),
5527
+ contextMenu: makeGridMenu(this._host),
5528
+ // The groups sit where the registry order puts them, not where the
5529
+ // alphabet would: profit, then trades, then positions, then orders.
5530
+ groupOrder: "rows",
5531
+ selection: "multi"
5532
+ }) : null;
5533
+ this._grid?.setState({ group: "category", hidden: ["category", "order"] });
5534
+ this._host.register(this);
5535
+ }
5536
+ dispose() {
5537
+ this._grid?.destroy();
5538
+ this._host.unregister(this);
5539
+ try {
5540
+ this.rootEl.remove();
5541
+ } catch {
5542
+ }
5543
+ }
5544
+ /// Show these statistics. The whole set at once: a strategy publishes its parameters as one
5545
+ /// table and a row that vanished from it has stopped existing, not stopped changing.
5546
+ update(rows) {
5547
+ this._rows = rank(rows || []);
5548
+ this._grid?.setRows(this._rows);
5549
+ }
5550
+ _export() {
5551
+ this._grid?.download("statistics", this._host.t("Statistics"));
5552
+ }
5553
+ // Two visible columns and one that only groups. The order column is hidden as well: it
5554
+ // decides the sort and means nothing to a reader.
5555
+ _columns() {
5556
+ const label = (key) => this._host.t(key);
5557
+ return [
5558
+ {
5559
+ key: "category",
5560
+ header: label("Category"),
5561
+ exportable: false,
5562
+ // The group's place, not its name: groups are laid out in the order the
5563
+ // registry gives their parameters, so profit comes before trade counts.
5564
+ // Grouping on the name would order them alphabetically, and on the
5565
+ // translated name would reorder them with the reader's language.
5566
+ value: (r) => r.categoryRank,
5567
+ text: (r) => r.categoryText || r.category
5568
+ },
5569
+ {
5570
+ key: "order",
5571
+ header: label("Order"),
5572
+ exportable: false,
5573
+ value: (r) => r.order
5574
+ },
5575
+ {
5576
+ key: "name",
5577
+ header: label("Name"),
5578
+ exportable: true,
5579
+ value: (r) => r.name,
5580
+ bindCell: (td, r) => {
5581
+ if (r.description) td.setAttribute("title", r.description);
5582
+ }
5583
+ },
5584
+ {
5585
+ key: "value",
5586
+ header: label("Value"),
5587
+ exportable: true,
5588
+ cellClass: () => "statistic-value",
5589
+ // Sorts on the raw value so a number sorts as a number, reads as the text below.
5590
+ value: (r) => r.value,
5591
+ render: (r) => formatStatistic(r.value),
5592
+ exportValue: (r) => r.value ?? ""
5593
+ }
5594
+ ];
5595
+ }
5596
+ };
5597
+ _StatisticsWidget.TYPE = ControlTypes.Statistics;
5598
+ var StatisticsWidget = _StatisticsWidget;
5599
+ function formatStatistic(value) {
5600
+ if (value === null || value === void 0 || value === "") return "";
5601
+ if (typeof value === "number") {
5602
+ if (!isFinite(value)) return "";
5603
+ return String(Math.round(value * 100) / 100);
5604
+ }
5605
+ if (value instanceof Date) return isoDate(value);
5606
+ if (typeof value === "string") {
5607
+ const at = /^\d{4}-\d{2}-\d{2}([T ]|$)/.test(value) ? new Date(value) : null;
5608
+ if (at !== null && !isNaN(at.getTime())) return isoDate(at);
5609
+ return value;
5610
+ }
5611
+ return String(value);
5612
+ }
5613
+ function isoDate(at) {
5614
+ return at.toISOString().slice(0, 10);
5615
+ }
5616
+
4357
5617
  // src/tradefeed-aggregator.ts
4358
5618
  function aggregateBubbles(ticks, maxBucketsPerSide) {
4359
5619
  if (!ticks || ticks.length === 0) return [];
@@ -4382,13 +5642,13 @@ var SSTradingControls = (() => {
4382
5642
  quantity += q;
4383
5643
  weighted += q * tick.price;
4384
5644
  }
4385
- const price = quantity > 0 ? weighted / quantity : slice[0].price;
5645
+ const price2 = quantity > 0 ? weighted / quantity : slice[0].price;
4386
5646
  const middle = slice[Math.floor(slice.length / 2)];
4387
5647
  return {
4388
5648
  symbol: slice[0].symbol,
4389
5649
  side: slice[0].side,
4390
5650
  buy: slice[0].buy,
4391
- price,
5651
+ price: price2,
4392
5652
  quantity,
4393
5653
  time: middle.time,
4394
5654
  index: middle.index,
@@ -4483,7 +5743,7 @@ var SSTradingControls = (() => {
4483
5743
  if (magnitude >= 1e3) digits = 0;
4484
5744
  else if (magnitude >= 10) digits = 2;
4485
5745
  else if (magnitude >= 0.1) digits = 4;
4486
- return (price) => price.toLocaleString(void 0, {
5746
+ return (price2) => price2.toLocaleString(void 0, {
4487
5747
  minimumFractionDigits: digits,
4488
5748
  maximumFractionDigits: digits
4489
5749
  });