@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.
- package/README.md +95 -16
- package/dist/esm/active-orders-widget.js +38 -25
- package/dist/esm/active-orders-widget.js.map +1 -1
- package/dist/esm/black-scholes.js +147 -0
- package/dist/esm/black-scholes.js.map +1 -0
- package/dist/esm/control-types.js +4 -0
- package/dist/esm/control-types.js.map +1 -1
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/log-monitor-widget.js +265 -0
- package/dist/esm/log-monitor-widget.js.map +1 -0
- package/dist/esm/log-tree.js +96 -0
- package/dist/esm/log-tree.js.map +1 -0
- package/dist/esm/option-desk-widget.js +322 -0
- package/dist/esm/option-desk-widget.js.map +1 -0
- package/dist/esm/pnl-curve.js +129 -0
- package/dist/esm/pnl-curve.js.map +1 -0
- package/dist/esm/statistics-widget.js +194 -0
- package/dist/esm/statistics-widget.js.map +1 -0
- package/dist/esm/strategies-widget.js +348 -0
- package/dist/esm/strategies-widget.js.map +1 -0
- package/dist/esm/trade-history-widget.js +14 -6
- package/dist/esm/trade-history-widget.js.map +1 -1
- package/dist/esm/tradefeed-widget.js +3 -3
- package/dist/esm/tradefeed-widget.js.map +1 -1
- package/dist/esm/trading-host.js +1 -0
- package/dist/esm/trading-host.js.map +1 -1
- package/dist/sstradingcontrols.js +1358 -78
- package/dist/sstradingcontrols.js.map +4 -4
- package/dist/types/active-orders-widget.d.ts +11 -3
- package/dist/types/active-orders-widget.d.ts.map +1 -1
- package/dist/types/black-scholes.d.ts +28 -0
- package/dist/types/black-scholes.d.ts.map +1 -0
- package/dist/types/control-types.d.ts +4 -0
- package/dist/types/control-types.d.ts.map +1 -1
- package/dist/types/index.d.ts +16 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/log-monitor-widget.d.ts +42 -0
- package/dist/types/log-monitor-widget.d.ts.map +1 -0
- package/dist/types/log-tree.d.ts +34 -0
- package/dist/types/log-tree.d.ts.map +1 -0
- package/dist/types/option-desk-widget.d.ts +68 -0
- package/dist/types/option-desk-widget.d.ts.map +1 -0
- package/dist/types/pnl-curve.d.ts +43 -0
- package/dist/types/pnl-curve.d.ts.map +1 -0
- package/dist/types/statistics-widget.d.ts +29 -0
- package/dist/types/statistics-widget.d.ts.map +1 -0
- package/dist/types/strategies-widget.d.ts +63 -0
- package/dist/types/strategies-widget.d.ts.map +1 -0
- package/dist/types/trade-history-widget.d.ts +3 -1
- package/dist/types/trade-history-widget.d.ts.map +1 -1
- package/dist/types/trading-data.d.ts +9 -0
- package/dist/types/trading-data.d.ts.map +1 -1
- package/dist/types/trading-host.d.ts +1 -0
- package/dist/types/trading-host.d.ts.map +1 -1
- package/package.json +27 -2
- package/screenshots/log-monitor.png +0 -0
- package/screenshots/option-desk.png +0 -0
- package/screenshots/panels.jpg +0 -0
- package/screenshots/statistics.png +0 -0
- package/screenshots/strategies.png +0 -0
- package/src/active-orders-widget.ts +49 -25
- package/src/black-scholes.ts +199 -0
- package/src/control-types.ts +4 -0
- package/src/index.ts +41 -0
- package/src/log-monitor-widget.ts +312 -0
- package/src/log-tree.ts +131 -0
- package/src/option-desk-widget.ts +422 -0
- package/src/pnl-curve.ts +204 -0
- package/src/statistics-widget.ts +226 -0
- package/src/strategies-widget.ts +435 -0
- package/src/trade-history-widget.ts +19 -6
- package/src/tradefeed-widget.ts +3 -3
- package/src/trading-data.ts +22 -0
- package/src/trading-host.ts +8 -0
- package/styles/trading-controls.css +356 -0
- 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(
|
|
127
|
-
if (
|
|
128
|
-
const n = Number(
|
|
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
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
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.
|
|
1979
|
+
this._actions?.cancelAllOrders();
|
|
1929
1980
|
});
|
|
1930
1981
|
this._refreshBtn?.addEventListener("click", (e) => {
|
|
1931
1982
|
e.preventDefault();
|
|
1932
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
2193
|
-
|
|
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) =>
|
|
2572
|
-
exportValue: (t) =>
|
|
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,
|
|
2776
|
-
if (
|
|
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 =
|
|
2792
|
-
this._host.cache.set(baselineKey, String(
|
|
2852
|
+
baseline = price2;
|
|
2853
|
+
this._host.cache.set(baselineKey, String(price2));
|
|
2793
2854
|
}
|
|
2794
|
-
const chgPct = baseline ? (
|
|
2795
|
-
this.stats.set(full, { ...cur || {}, lastPrice:
|
|
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(
|
|
3002
|
-
const n = Number(
|
|
3003
|
-
if (
|
|
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(
|
|
3276
|
-
this._lastPrice =
|
|
3277
|
-
this._writePrices(this._bestBid > 0 ? this._bestBid :
|
|
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(
|
|
3282
|
-
this._lastPrice =
|
|
3283
|
-
this._writePrices(this._bestBid > 0 ? this._bestBid :
|
|
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,
|
|
3369
|
+
setPrice(side, price2) {
|
|
3309
3370
|
const input = this._input(side, PRICE_INPUT);
|
|
3310
3371
|
if (!input) return;
|
|
3311
|
-
const text = this._formatPriceForInput(
|
|
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,
|
|
3604
|
+
_writePrice(side, price2, reset) {
|
|
3544
3605
|
const input = this._input(side, PRICE_INPUT);
|
|
3545
3606
|
if (!input) return;
|
|
3546
|
-
const text = this._formatPriceForInput(
|
|
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(
|
|
3560
|
-
const n = Number(
|
|
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
|
|
3639
|
+
let price2;
|
|
3579
3640
|
if (this._orderType === OrderEntryTypes.Limit || this._orderType === OrderEntryTypes.StopLimit) {
|
|
3580
|
-
|
|
3641
|
+
price2 = this._number(side, PRICE_INPUT) ?? 0;
|
|
3581
3642
|
} else if (this._orderType === OrderEntryTypes.Stop) {
|
|
3582
|
-
|
|
3643
|
+
price2 = this._number(side, STOP_INPUT) ?? this._lastPrice;
|
|
3583
3644
|
} else {
|
|
3584
3645
|
const bbo = side === OrderEntrySides.Buy ? this._bestAsk : this._bestBid;
|
|
3585
|
-
|
|
3646
|
+
price2 = bbo > 0 ? bbo : this._lastPrice;
|
|
3586
3647
|
}
|
|
3587
|
-
total.textContent = quantity > 0 &&
|
|
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,
|
|
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 +
|
|
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 +
|
|
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
|
|
3993
|
-
if (!Number.isFinite(
|
|
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(
|
|
3996
|
-
else this._deps.onPriceSelected(
|
|
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,
|
|
4056
|
-
return [...ledger.entries()].filter(([
|
|
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
|
|
4097
|
-
for (const
|
|
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(
|
|
4234
|
-
return Number(
|
|
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
|
|
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 (
|
|
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) =>
|
|
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"),
|
|
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)));
|