@stocksharp/trading-controls 1.1.1 → 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 (77) hide show
  1. package/README.md +95 -16
  2. package/dist/esm/active-orders-widget.js +38 -25
  3. package/dist/esm/active-orders-widget.js.map +1 -1
  4. package/dist/esm/black-scholes.js +147 -0
  5. package/dist/esm/black-scholes.js.map +1 -0
  6. package/dist/esm/control-types.js +4 -0
  7. package/dist/esm/control-types.js.map +1 -1
  8. package/dist/esm/index.js +18 -0
  9. package/dist/esm/index.js.map +1 -1
  10. package/dist/esm/log-monitor-widget.js +265 -0
  11. package/dist/esm/log-monitor-widget.js.map +1 -0
  12. package/dist/esm/log-tree.js +96 -0
  13. package/dist/esm/log-tree.js.map +1 -0
  14. package/dist/esm/option-desk-widget.js +322 -0
  15. package/dist/esm/option-desk-widget.js.map +1 -0
  16. package/dist/esm/pnl-curve.js +129 -0
  17. package/dist/esm/pnl-curve.js.map +1 -0
  18. package/dist/esm/statistics-widget.js +194 -0
  19. package/dist/esm/statistics-widget.js.map +1 -0
  20. package/dist/esm/strategies-widget.js +348 -0
  21. package/dist/esm/strategies-widget.js.map +1 -0
  22. package/dist/esm/trade-history-widget.js +14 -6
  23. package/dist/esm/trade-history-widget.js.map +1 -1
  24. package/dist/esm/tradefeed-widget.js +3 -3
  25. package/dist/esm/tradefeed-widget.js.map +1 -1
  26. package/dist/esm/trading-host.js +1 -0
  27. package/dist/esm/trading-host.js.map +1 -1
  28. package/dist/sstradingcontrols.js +1358 -78
  29. package/dist/sstradingcontrols.js.map +4 -4
  30. package/dist/types/active-orders-widget.d.ts +11 -3
  31. package/dist/types/active-orders-widget.d.ts.map +1 -1
  32. package/dist/types/black-scholes.d.ts +28 -0
  33. package/dist/types/black-scholes.d.ts.map +1 -0
  34. package/dist/types/control-types.d.ts +4 -0
  35. package/dist/types/control-types.d.ts.map +1 -1
  36. package/dist/types/index.d.ts +16 -1
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/log-monitor-widget.d.ts +42 -0
  39. package/dist/types/log-monitor-widget.d.ts.map +1 -0
  40. package/dist/types/log-tree.d.ts +34 -0
  41. package/dist/types/log-tree.d.ts.map +1 -0
  42. package/dist/types/option-desk-widget.d.ts +68 -0
  43. package/dist/types/option-desk-widget.d.ts.map +1 -0
  44. package/dist/types/pnl-curve.d.ts +43 -0
  45. package/dist/types/pnl-curve.d.ts.map +1 -0
  46. package/dist/types/statistics-widget.d.ts +29 -0
  47. package/dist/types/statistics-widget.d.ts.map +1 -0
  48. package/dist/types/strategies-widget.d.ts +63 -0
  49. package/dist/types/strategies-widget.d.ts.map +1 -0
  50. package/dist/types/trade-history-widget.d.ts +3 -1
  51. package/dist/types/trade-history-widget.d.ts.map +1 -1
  52. package/dist/types/trading-data.d.ts +9 -0
  53. package/dist/types/trading-data.d.ts.map +1 -1
  54. package/dist/types/trading-host.d.ts +1 -0
  55. package/dist/types/trading-host.d.ts.map +1 -1
  56. package/package.json +27 -2
  57. package/screenshots/log-monitor.png +0 -0
  58. package/screenshots/option-desk.png +0 -0
  59. package/screenshots/panels.jpg +0 -0
  60. package/screenshots/statistics.png +0 -0
  61. package/screenshots/strategies.png +0 -0
  62. package/src/active-orders-widget.ts +49 -25
  63. package/src/black-scholes.ts +199 -0
  64. package/src/control-types.ts +4 -0
  65. package/src/index.ts +41 -0
  66. package/src/log-monitor-widget.ts +312 -0
  67. package/src/log-tree.ts +131 -0
  68. package/src/option-desk-widget.ts +422 -0
  69. package/src/pnl-curve.ts +204 -0
  70. package/src/statistics-widget.ts +226 -0
  71. package/src/strategies-widget.ts +435 -0
  72. package/src/trade-history-widget.ts +19 -6
  73. package/src/tradefeed-widget.ts +3 -3
  74. package/src/trading-data.ts +22 -0
  75. package/src/trading-host.ts +8 -0
  76. package/styles/trading-controls.css +356 -0
  77. 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
@@ -73,6 +99,7 @@ var SSTradingControls = (() => {
73
99
  _required(host.presentation.isBuy, "host.presentation.isBuy", "function", controlName);
74
100
  _required(host.presentation.typeText, "host.presentation.typeText", "function", controlName);
75
101
  _required(host.presentation.statusText, "host.presentation.statusText", "function", controlName);
102
+ _required(host.presentation.timeText, "host.presentation.timeText", "function", controlName);
76
103
  _required(host.presentation.sideClass, "host.presentation.sideClass", "function", controlName);
77
104
  _required(host.presentation.pnlClass, "host.presentation.pnlClass", "function", controlName);
78
105
  _required(host.presentation.canvasPalette, "host.presentation.canvasPalette", "function", controlName);
@@ -119,13 +146,17 @@ var SSTradingControls = (() => {
119
146
  Positions: "positions",
120
147
  OrderBook: "orderbook",
121
148
  TradeFeed: "tradefeed",
122
- OrderEntry: "orderEntry"
149
+ OrderEntry: "orderEntry",
150
+ Statistics: "statistics",
151
+ LogMonitor: "logMonitor",
152
+ Strategies: "strategies",
153
+ OptionDesk: "optionDesk"
123
154
  };
124
155
 
125
156
  // src/formatters.ts
126
- function formatPrice(price) {
127
- if (price == null || isNaN(Number(price))) return "--";
128
- const n = Number(price);
157
+ function formatPrice(price2) {
158
+ if (price2 == null || isNaN(Number(price2))) return "--";
159
+ const n = Number(price2);
129
160
  const a = Math.abs(n);
130
161
  let prec = 2;
131
162
  if (a < 1) prec = 4;
@@ -814,6 +845,13 @@ var SSTradingControls = (() => {
814
845
  copyRows: (count) => `Copy selected rows (${count})`,
815
846
  exportXlsx: "Export to .xlsx"
816
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
+ }
817
855
  var _DataGrid = class _DataGrid {
818
856
  constructor(options) {
819
857
  __publicField(this, "options");
@@ -1726,6 +1764,7 @@ var SSTradingControls = (() => {
1726
1764
  tr.addEventListener("mousedown", (event) => {
1727
1765
  const button = event.button;
1728
1766
  if (button !== void 0 && button !== 0) return;
1767
+ if (isControl(event)) return;
1729
1768
  event.preventDefault();
1730
1769
  _DataGrid._active = this;
1731
1770
  this._clickRow(key, event);
@@ -1787,6 +1826,7 @@ var SSTradingControls = (() => {
1787
1826
  if (group) group.rows.push(row);
1788
1827
  else groups.set(key, { label: col ? this._displayText(col, row) : key, rows: [row] });
1789
1828
  }
1829
+ if (this.options.groupOrder === "rows") return groups;
1790
1830
  return new Map([...groups.entries()].sort((a, b) => this._collator.compare(a[1].label, b[1].label)));
1791
1831
  }
1792
1832
  _groupRow(key, label, count) {
@@ -1801,6 +1841,8 @@ var SSTradingControls = (() => {
1801
1841
  else td.appendChild(content);
1802
1842
  tr.appendChild(td);
1803
1843
  tr.addEventListener("click", () => this.toggleGroup(key));
1844
+ tr.style.cursor = "pointer";
1845
+ tr.style.userSelect = "none";
1804
1846
  return tr;
1805
1847
  }
1806
1848
  _emptyRow() {
@@ -1863,7 +1905,7 @@ var SSTradingControls = (() => {
1863
1905
  var _ActiveOrdersWidget = class _ActiveOrdersWidget {
1864
1906
  static create(hostEl, state, deps) {
1865
1907
  const host = assertHost(deps?.host, "ActiveOrdersWidget");
1866
- const root = _ActiveOrdersWidget._buildRoot(host);
1908
+ const root = _ActiveOrdersWidget._buildRoot(host, deps?.readOnly === true);
1867
1909
  root.id = makePanelId(_ActiveOrdersWidget.TYPE);
1868
1910
  hostEl.appendChild(root);
1869
1911
  return new _ActiveOrdersWidget(root, state || {}, deps);
@@ -1879,7 +1921,7 @@ var SSTradingControls = (() => {
1879
1921
  // already give the identical button — and are wired to deps. Cancel-all
1880
1922
  // keeps a distinct accessible name from its tooltip, which is why it states
1881
1923
  // `aria-label` rather than taking the tooltip for both.
1882
- static _buildRoot(host) {
1924
+ static _buildRoot(host, readOnly = false) {
1883
1925
  return makePanelRoot("active-orders-panel", host.t("ActiveOrders"), [
1884
1926
  makeElement("div", "panel-header", {}, [
1885
1927
  makeElement("span", "", {}, [host.t("OpenOrders")]),
@@ -1893,13 +1935,15 @@ var SSTradingControls = (() => {
1893
1935
  ])
1894
1936
  ]),
1895
1937
  makeElement("div", "panel-rail", { role: "toolbar", "aria-label": host.t("ActiveOrdersActions") }, [
1896
- makeIconButton(
1897
- "bt-icon-btn bt-icon-cancel-all panel-cancel-all-btn",
1898
- host.t("CancelAll"),
1899
- "bi-x-circle",
1900
- { "aria-label": host.t("CancelAllOrders") }
1901
- ),
1902
- makeIconButton("bt-icon-btn panel-refresh-btn", host.t("Refresh"), "bi-arrow-clockwise", {}),
1938
+ ...readOnly ? [] : [
1939
+ makeIconButton(
1940
+ "bt-icon-btn bt-icon-cancel-all panel-cancel-all-btn",
1941
+ host.t("CancelAll"),
1942
+ "bi-x-circle",
1943
+ { "aria-label": host.t("CancelAllOrders") }
1944
+ ),
1945
+ makeIconButton("bt-icon-btn panel-refresh-btn", host.t("Refresh"), "bi-arrow-clockwise", {})
1946
+ ],
1903
1947
  makeIconButton("bt-icon-btn panel-export-btn", host.t("ExportToExcel"), "bi-file-earmark-spreadsheet", {})
1904
1948
  ])
1905
1949
  ])
@@ -1907,9 +1951,16 @@ var SSTradingControls = (() => {
1907
1951
  }
1908
1952
  constructor(rootEl, _state, deps) {
1909
1953
  this._host = assertHost(deps?.host, "ActiveOrdersWidget");
1910
- for (const name of ["cancelOrder", "dismissOrder", "editOrderField", "replaceOrder", "cancelAllOrders", "refreshOrders"]) {
1911
- if (typeof deps?.[name] !== "function")
1912
- throw new Error(`ActiveOrdersWidget: dep "${name}" is required`);
1954
+ this._readOnly = deps?.readOnly === true;
1955
+ if (this._readOnly) {
1956
+ this._actions = null;
1957
+ } else {
1958
+ const actions = deps;
1959
+ for (const name of ["cancelOrder", "dismissOrder", "editOrderField", "replaceOrder", "cancelAllOrders", "refreshOrders"]) {
1960
+ if (typeof actions?.[name] !== "function")
1961
+ throw new Error(`ActiveOrdersWidget: dep "${name}" is required`);
1962
+ }
1963
+ this._actions = actions;
1913
1964
  }
1914
1965
  this.rootEl = rootEl;
1915
1966
  this._deps = deps;
@@ -1925,11 +1976,11 @@ var SSTradingControls = (() => {
1925
1976
  });
1926
1977
  this._cancelAllBtn?.addEventListener("click", (e) => {
1927
1978
  e.preventDefault();
1928
- this._deps.cancelAllOrders();
1979
+ this._actions?.cancelAllOrders();
1929
1980
  });
1930
1981
  this._refreshBtn?.addEventListener("click", (e) => {
1931
1982
  e.preventDefault();
1932
- this._deps.refreshOrders();
1983
+ this._actions?.refreshOrders();
1933
1984
  });
1934
1985
  this._exportBtn?.addEventListener("click", (e) => {
1935
1986
  e.preventDefault();
@@ -2032,7 +2083,7 @@ var SSTradingControls = (() => {
2032
2083
  }
2033
2084
  const updated = { quantity: order.quantity, limitPrice: order.limitPrice, stopPrice: order.stopPrice };
2034
2085
  updated[field] = newVal;
2035
- this._deps.replaceOrder(orderId, updated.quantity, updated.limitPrice, updated.stopPrice);
2086
+ this._actions?.replaceOrder(orderId, updated.quantity, updated.limitPrice, updated.stopPrice);
2036
2087
  };
2037
2088
  input.addEventListener("blur", commit);
2038
2089
  input.addEventListener("keydown", (e) => {
@@ -2092,7 +2143,7 @@ var SSTradingControls = (() => {
2092
2143
  exportable: true,
2093
2144
  value: (o) => o.quantity,
2094
2145
  render: (o) => formatQty(o.quantity),
2095
- cellClass: (o) => _ActiveOrdersWidget._editableClass(o, "quantity"),
2146
+ cellClass: (o) => this._readOnly ? "" : _ActiveOrdersWidget._editableClass(o, "quantity"),
2096
2147
  bindCell: (td, o) => this._bindInlineEdit(td, o, "quantity")
2097
2148
  },
2098
2149
  {
@@ -2101,7 +2152,7 @@ var SSTradingControls = (() => {
2101
2152
  exportable: true,
2102
2153
  value: (o) => o.limitPrice,
2103
2154
  render: (o) => o.limitPrice ? formatPrice(o.limitPrice) : label("MKT"),
2104
- cellClass: (o) => _ActiveOrdersWidget._editableClass(o, "limitPrice"),
2155
+ cellClass: (o) => this._readOnly ? "" : _ActiveOrdersWidget._editableClass(o, "limitPrice"),
2105
2156
  bindCell: (td, o) => this._bindInlineEdit(td, o, "limitPrice"),
2106
2157
  exportValue: (o) => o.limitPrice ?? label("MKT")
2107
2158
  },
@@ -2111,7 +2162,7 @@ var SSTradingControls = (() => {
2111
2162
  exportable: true,
2112
2163
  value: (o) => o.stopPrice,
2113
2164
  render: (o) => o.stopPrice ? formatPrice(o.stopPrice) : "--",
2114
- cellClass: (o) => _ActiveOrdersWidget._editableClass(o, "stopPrice"),
2165
+ cellClass: (o) => this._readOnly ? "" : _ActiveOrdersWidget._editableClass(o, "stopPrice"),
2115
2166
  bindCell: (td, o) => this._bindInlineEdit(td, o, "stopPrice"),
2116
2167
  exportValue: (o) => o.stopPrice ?? ""
2117
2168
  },
@@ -2123,14 +2174,14 @@ var SSTradingControls = (() => {
2123
2174
  render: (o) => this._statusCell(o),
2124
2175
  exportValue: (o) => presentation.statusText(o.status)
2125
2176
  },
2126
- {
2177
+ ...this._readOnly ? [] : [{
2127
2178
  key: "actions",
2128
2179
  header: label("Actions"),
2129
2180
  headerHidden: true,
2130
2181
  exportable: false,
2131
2182
  cellClass: () => "position-actions",
2132
2183
  render: (o) => this._actionButton(o)
2133
- }
2184
+ }]
2134
2185
  ];
2135
2186
  }
2136
2187
  static _rowClass(order) {
@@ -2152,8 +2203,9 @@ var SSTradingControls = (() => {
2152
2203
  // Double-click to edit belongs to the whole cell rather than to a control
2153
2204
  // inside it, which is why it is wired here instead of returned by render().
2154
2205
  _bindInlineEdit(td, order, field) {
2155
- if (!_ActiveOrdersWidget._canEdit(order, field)) return;
2156
- td.addEventListener("dblclick", () => this._deps.editOrderField(order.id, field));
2206
+ if (this._actions === null || !_ActiveOrdersWidget._canEdit(order, field)) return;
2207
+ const edit = this._actions.editOrderField;
2208
+ td.addEventListener("dblclick", () => edit(order.id, field));
2157
2209
  }
2158
2210
  // Status text, plus a hoverable icon carrying the rejection reason. Some
2159
2211
  // venues wrap the human-readable rejection inside a JSON blob;
@@ -2189,8 +2241,9 @@ var SSTradingControls = (() => {
2189
2241
  icon.className = "bi bi-x-circle";
2190
2242
  button.appendChild(icon);
2191
2243
  button.addEventListener("click", () => {
2192
- if (isTerminal) this._deps.dismissOrder(order.id);
2193
- else this._deps.cancelOrder(order.id);
2244
+ if (this._actions === null) return;
2245
+ if (isTerminal) this._actions.dismissOrder(order.id);
2246
+ else this._actions.cancelOrder(order.id);
2194
2247
  });
2195
2248
  return button;
2196
2249
  }
@@ -2447,14 +2500,14 @@ var SSTradingControls = (() => {
2447
2500
  var _TradeHistoryWidget = class _TradeHistoryWidget {
2448
2501
  static create(hostEl, state, deps) {
2449
2502
  const host = assertHost(deps?.host, "TradeHistoryWidget");
2450
- const root = _TradeHistoryWidget._buildRoot(host);
2503
+ const root = _TradeHistoryWidget._buildRoot(host, deps?.readOnly === true);
2451
2504
  root.id = makePanelId(_TradeHistoryWidget.TYPE);
2452
2505
  hostEl.appendChild(root);
2453
2506
  return new _TradeHistoryWidget(root, state || {}, deps);
2454
2507
  }
2455
2508
  // The panel's markup. The host stylesheet reads this structure, and a
2456
2509
  // docking host lifts `.panel-header`'s children into its tab strip.
2457
- static _buildRoot(host) {
2510
+ static _buildRoot(host, readOnly = false) {
2458
2511
  const title = host.t("TradeHistory");
2459
2512
  return makePanelRoot("trade-history-panel", title, [
2460
2513
  makeElement("div", "panel-header", {}, [
@@ -2469,7 +2522,7 @@ var SSTradingControls = (() => {
2469
2522
  ])
2470
2523
  ]),
2471
2524
  makeElement("div", "panel-rail", { role: "toolbar", "aria-label": host.t("TradeHistoryActions") }, [
2472
- makeIconButton("bt-icon-btn panel-refresh-btn", host.t("Refresh"), "bi-arrow-clockwise", {}),
2525
+ ...readOnly ? [] : [makeIconButton("bt-icon-btn panel-refresh-btn", host.t("Refresh"), "bi-arrow-clockwise", {})],
2473
2526
  makeIconButton("bt-icon-btn panel-export-btn", host.t("ExportToExcel"), "bi-file-earmark-spreadsheet", {})
2474
2527
  ])
2475
2528
  ])
@@ -2545,6 +2598,14 @@ var SSTradingControls = (() => {
2545
2598
  this._host.log(`TradeHistoryWidget: failed to load trade history: ${err}`);
2546
2599
  }
2547
2600
  }
2601
+ /// Show these rows. The push path, for a consumer that already holds the
2602
+ /// trades - a finished run's report - rather than an account to load them
2603
+ /// from. `refresh` is the pull path and the two do not mix: whichever ran
2604
+ /// last is what the grid shows.
2605
+ update(rows) {
2606
+ this._rows = rows || [];
2607
+ this._grid?.setRows(this._rows);
2608
+ }
2548
2609
  // Export the table to .xlsx in the currently rendered (sorted) order.
2549
2610
  _export() {
2550
2611
  this._grid?.download("trades", this._host.t("TradeHistory"));
@@ -2568,8 +2629,8 @@ var SSTradingControls = (() => {
2568
2629
  header: label("Time"),
2569
2630
  exportable: true,
2570
2631
  value: (t) => t.executedAt || t.time,
2571
- render: (t) => formatTime(t.executedAt || t.time),
2572
- exportValue: (t) => formatTime(t.executedAt || t.time)
2632
+ render: (t) => presentation.timeText(t.executedAt || t.time),
2633
+ exportValue: (t) => presentation.timeText(t.executedAt || t.time)
2573
2634
  },
2574
2635
  {
2575
2636
  key: "symbol",
@@ -2772,8 +2833,8 @@ var SSTradingControls = (() => {
2772
2833
  if (previous) this._grid.rowElement(String(previous))?.classList.remove("wl-current");
2773
2834
  if (symbol) this._grid.rowElement(String(symbol))?.classList.add("wl-current");
2774
2835
  }
2775
- onPriceUpdate(symbol, price) {
2776
- if (price == null || !isFinite(price)) return;
2836
+ onPriceUpdate(symbol, price2) {
2837
+ if (price2 == null || !isFinite(price2)) return;
2777
2838
  let full = symbol;
2778
2839
  if (!full.includes("@")) {
2779
2840
  const match = this.instruments.find((i) => String(i.symbol).split("@")[0] === symbol);
@@ -2788,11 +2849,11 @@ var SSTradingControls = (() => {
2788
2849
  baseline = stored ? Number(stored) : void 0;
2789
2850
  }
2790
2851
  if (baseline == null || !isFinite(baseline) || baseline === 0) {
2791
- baseline = price;
2792
- this._host.cache.set(baselineKey, String(price));
2852
+ baseline = price2;
2853
+ this._host.cache.set(baselineKey, String(price2));
2793
2854
  }
2794
- const chgPct = baseline ? (price - baseline) / baseline * 100 : 0;
2795
- 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 });
2796
2857
  this._updateRow(full, prev);
2797
2858
  }
2798
2859
  static _baselineKey(symbol) {
@@ -2998,9 +3059,9 @@ var SSTradingControls = (() => {
2998
3059
  if (!this._host.isPrimary) return;
2999
3060
  this._host.ticker.publish(this._visible().map((i) => String(i.symbol)), this.stats);
3000
3061
  }
3001
- static _priceText(price) {
3002
- const n = Number(price);
3003
- 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 "--";
3004
3065
  if (n >= 1) return n.toFixed(2);
3005
3066
  if (n >= 0.01) return n.toFixed(4);
3006
3067
  return n.toFixed(6);
@@ -3272,15 +3333,15 @@ var SSTradingControls = (() => {
3272
3333
  }
3273
3334
  /// A reference price off the tape. Both columns follow the market while the
3274
3335
  /// form is untouched; the field the trader is typing in is left alone.
3275
- setLimitPrice(price) {
3276
- this._lastPrice = price;
3277
- 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);
3278
3339
  }
3279
3340
  /// The same reference price, force-written: an explicit host gesture
3280
3341
  /// ("price this order here") outranks whatever was typed.
3281
- seedLimitPrice(price) {
3282
- this._lastPrice = price;
3283
- 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);
3284
3345
  }
3285
3346
  /// Best bid and offer. The passive seed for a limit form is Buy = best bid
3286
3347
  /// and Sell = best ask — joining the book on each side, so the spread in the
@@ -3305,10 +3366,10 @@ var SSTradingControls = (() => {
3305
3366
  /// Force one column's price and let ticks resume updating it afterwards —
3306
3367
  /// what a click on a book level or a chart price means. Quantized to the
3307
3368
  /// instrument's tick, so a raw chart coordinate does not fail validation.
3308
- setPrice(side, price) {
3369
+ setPrice(side, price2) {
3309
3370
  const input = this._input(side, PRICE_INPUT);
3310
3371
  if (!input) return;
3311
- const text = this._formatPriceForInput(price);
3372
+ const text = this._formatPriceForInput(price2);
3312
3373
  if (!text) return;
3313
3374
  input.dataset.userEdited = "0";
3314
3375
  input.value = text;
@@ -3540,10 +3601,10 @@ var SSTradingControls = (() => {
3540
3601
  this._writePrice(OrderEntrySides.Sell, sellPrice, reset);
3541
3602
  for (const side of _OrderEntryWidget.SIDES) this._recalculate(side);
3542
3603
  }
3543
- _writePrice(side, price, reset) {
3604
+ _writePrice(side, price2, reset) {
3544
3605
  const input = this._input(side, PRICE_INPUT);
3545
3606
  if (!input) return;
3546
- const text = this._formatPriceForInput(price);
3607
+ const text = this._formatPriceForInput(price2);
3547
3608
  if (!text) return;
3548
3609
  if (reset) {
3549
3610
  input.dataset.userEdited = "0";
@@ -3556,8 +3617,8 @@ var SSTradingControls = (() => {
3556
3617
  }
3557
3618
  // Round a price to the instrument's tick and cut the float tail: a mid price
3558
3619
  // of (bid+ask)/2 otherwise reaches the field as 0.44625000000000004.
3559
- _formatPriceForInput(price) {
3560
- const n = Number(price);
3620
+ _formatPriceForInput(price2) {
3621
+ const n = Number(price2);
3561
3622
  if (!isFinite(n)) return "";
3562
3623
  const tickSize = Number(this._instrument?.tickSize) || 0;
3563
3624
  if (tickSize > 0)
@@ -3575,16 +3636,16 @@ var SSTradingControls = (() => {
3575
3636
  const total = this._cols[side]?.querySelector(TOTAL_VALUE);
3576
3637
  if (!total) return;
3577
3638
  const quantity = this._number(side, QTY_INPUT) ?? 0;
3578
- let price;
3639
+ let price2;
3579
3640
  if (this._orderType === OrderEntryTypes.Limit || this._orderType === OrderEntryTypes.StopLimit) {
3580
- price = this._number(side, PRICE_INPUT) ?? 0;
3641
+ price2 = this._number(side, PRICE_INPUT) ?? 0;
3581
3642
  } else if (this._orderType === OrderEntryTypes.Stop) {
3582
- price = this._number(side, STOP_INPUT) ?? this._lastPrice;
3643
+ price2 = this._number(side, STOP_INPUT) ?? this._lastPrice;
3583
3644
  } else {
3584
3645
  const bbo = side === OrderEntrySides.Buy ? this._bestAsk : this._bestBid;
3585
- price = bbo > 0 ? bbo : this._lastPrice;
3646
+ price2 = bbo > 0 ? bbo : this._lastPrice;
3586
3647
  }
3587
- 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 }) : "--";
3588
3649
  }
3589
3650
  // The estimate line doubles as the validation message and the submit button
3590
3651
  // follows it. A disabled pad reads as invalid for the same reason: there is
@@ -3614,20 +3675,20 @@ var SSTradingControls = (() => {
3614
3675
  var OrderEntryWidget = _OrderEntryWidget;
3615
3676
 
3616
3677
  // src/orderbook-depth.ts
3617
- function depthSide(levels, direction, geometry) {
3678
+ function depthSide(levels, direction2, geometry) {
3618
3679
  if (levels.length === 0) return null;
3619
3680
  const best = levels[0].price;
3620
3681
  const worst = levels[levels.length - 1].price;
3621
3682
  const span = Math.abs(best - worst);
3622
3683
  if (!(span > 0)) return null;
3623
3684
  const reach = geometry.halfWidth - geometry.pad;
3624
- const points = [{ x: geometry.midX + direction * geometry.pad, cumulative: 0 }];
3685
+ const points = [{ x: geometry.midX + direction2 * geometry.pad, cumulative: 0 }];
3625
3686
  let cumulative = 0;
3626
3687
  for (const level of levels) {
3627
3688
  cumulative += level.quantity;
3628
3689
  const distance = Math.abs(level.price - best);
3629
3690
  points.push({
3630
- x: geometry.midX + direction * (geometry.pad + distance / span * reach),
3691
+ x: geometry.midX + direction2 * (geometry.pad + distance / span * reach),
3631
3692
  cumulative
3632
3693
  });
3633
3694
  }
@@ -3989,11 +4050,11 @@ var SSTradingControls = (() => {
3989
4050
  const target = e.target;
3990
4051
  const row = target?.closest(".ob-row");
3991
4052
  if (!row) return;
3992
- const price = Number(row.dataset.price);
3993
- if (!Number.isFinite(price)) return;
4053
+ const price2 = Number(row.dataset.price);
4054
+ if (!Number.isFinite(price2)) return;
3994
4055
  const side = row.dataset.side === "buy" ? 0 : 1;
3995
- if (e.ctrlKey || e.metaKey) this._deps.onPriceExecuted(price, side);
3996
- else this._deps.onPriceSelected(price, side);
4056
+ if (e.ctrlKey || e.metaKey) this._deps.onPriceExecuted(price2, side);
4057
+ else this._deps.onPriceSelected(price2, side);
3997
4058
  });
3998
4059
  }
3999
4060
  _refreshDepthButtons() {
@@ -4052,8 +4113,8 @@ var SSTradingControls = (() => {
4052
4113
  static _deepest() {
4053
4114
  return _OrderBookWidget.DEPTHS[_OrderBookWidget.DEPTHS.length - 1];
4054
4115
  }
4055
- static _sorted(ledger, direction) {
4056
- 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 }));
4057
4118
  }
4058
4119
  _applySnapshotSide(ledger, levels, side, symbol, sequence) {
4059
4120
  for (const level of levels || []) {
@@ -4093,8 +4154,8 @@ var SSTradingControls = (() => {
4093
4154
  if (this._bidsByPrice.size === 0 || this._asksByPrice.size === 0) return;
4094
4155
  let bestBid = -Infinity;
4095
4156
  let bestAsk = Infinity;
4096
- for (const price of this._bidsByPrice.keys()) if (price > bestBid) bestBid = price;
4097
- 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;
4098
4159
  if (bestBid >= bestAsk) {
4099
4160
  this._anomaly(`crossed book: bid=${bestBid} >= ask=${bestAsk} sym=${symbol} seq=${sequence} ${wasSnapshot ? "snapshot" : "diff"}`);
4100
4161
  }
@@ -4230,8 +4291,8 @@ var SSTradingControls = (() => {
4230
4291
  }
4231
4292
  return { bids, asks };
4232
4293
  }
4233
- static _priceKey(price) {
4234
- return Number(price).toFixed(8);
4294
+ static _priceKey(price2) {
4295
+ return Number(price2).toFixed(8);
4235
4296
  }
4236
4297
  // The bid/ask split under the ladder, as one measurement the stylesheet
4237
4298
  // turns into two widths.
@@ -4334,6 +4395,1225 @@ var SSTradingControls = (() => {
4334
4395
  _OrderBookWidget.DEPTHCHART_KEY = "terminal.obDepthChart";
4335
4396
  var OrderBookWidget = _OrderBookWidget;
4336
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
+
4337
5617
  // src/tradefeed-aggregator.ts
4338
5618
  function aggregateBubbles(ticks, maxBucketsPerSide) {
4339
5619
  if (!ticks || ticks.length === 0) return [];
@@ -4362,13 +5642,13 @@ var SSTradingControls = (() => {
4362
5642
  quantity += q;
4363
5643
  weighted += q * tick.price;
4364
5644
  }
4365
- const price = quantity > 0 ? weighted / quantity : slice[0].price;
5645
+ const price2 = quantity > 0 ? weighted / quantity : slice[0].price;
4366
5646
  const middle = slice[Math.floor(slice.length / 2)];
4367
5647
  return {
4368
5648
  symbol: slice[0].symbol,
4369
5649
  side: slice[0].side,
4370
5650
  buy: slice[0].buy,
4371
- price,
5651
+ price: price2,
4372
5652
  quantity,
4373
5653
  time: middle.time,
4374
5654
  index: middle.index,
@@ -4463,7 +5743,7 @@ var SSTradingControls = (() => {
4463
5743
  if (magnitude >= 1e3) digits = 0;
4464
5744
  else if (magnitude >= 10) digits = 2;
4465
5745
  else if (magnitude >= 0.1) digits = 4;
4466
- return (price) => price.toLocaleString(void 0, {
5746
+ return (price2) => price2.toLocaleString(void 0, {
4467
5747
  minimumFractionDigits: digits,
4468
5748
  maximumFractionDigits: digits
4469
5749
  });
@@ -4713,7 +5993,7 @@ var SSTradingControls = (() => {
4713
5993
  header: label("Time"),
4714
5994
  exportable: true,
4715
5995
  value: (t) => String(t.time || t.executedAt || ""),
4716
- render: (t) => formatTime(t.time || t.executedAt)
5996
+ render: (t) => this._host.presentation.timeText(t.time || t.executedAt)
4717
5997
  },
4718
5998
  {
4719
5999
  key: "symbol",
@@ -5018,7 +6298,7 @@ var SSTradingControls = (() => {
5018
6298
  const side = bubble.side;
5019
6299
  const lines = [];
5020
6300
  if (this._extraSymbols.size > 0) lines.push(this._tooltipLine(this._host.t("Symbol"), bubble.symbol, ""));
5021
- lines.push(this._tooltipLine(this._host.t("Time"), formatTime(bubble.time), ""));
6301
+ lines.push(this._tooltipLine(this._host.t("Time"), this._host.presentation.timeText(bubble.time), ""));
5022
6302
  lines.push(this._tooltipLine(aggregated ? this._host.t("VWAP") : this._host.t("Price"), formatPrice(bubble.price), ""));
5023
6303
  lines.push(this._tooltipLine(aggregated ? this._host.t("Total qty") : this._host.t("Qty"), formatQty(bubble.quantity), ""));
5024
6304
  lines.push(this._tooltipLine(this._host.t("Side"), presentation.sideText(side), presentation.sideClass(side)));