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