@stocksharp/trading-controls 1.2.0 → 1.4.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 +204 -17
- package/dist/esm/black-scholes.js +147 -0
- package/dist/esm/black-scholes.js.map +1 -0
- package/dist/esm/chart-engine.js +15 -0
- package/dist/esm/chart-engine.js.map +1 -0
- package/dist/esm/control-types.js +8 -0
- package/dist/esm/control-types.js.map +1 -1
- package/dist/esm/equity-widget.js +261 -0
- package/dist/esm/equity-widget.js.map +1 -0
- package/dist/esm/heatmap-grid.js +271 -0
- package/dist/esm/heatmap-grid.js.map +1 -0
- package/dist/esm/index.js +37 -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/optimization-heatmap-widget.js +303 -0
- package/dist/esm/optimization-heatmap-widget.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/option-smile-widget.js +252 -0
- package/dist/esm/option-smile-widget.js.map +1 -0
- package/dist/esm/pnl-curve.js +133 -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/surface-grid.js +282 -0
- package/dist/esm/surface-grid.js.map +1 -0
- package/dist/esm/surface-widget.js +379 -0
- package/dist/esm/surface-widget.js.map +1 -0
- package/dist/sstradingcontrols.js +2982 -295
- 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/chart-engine.d.ts +3 -0
- package/dist/types/chart-engine.d.ts.map +1 -0
- package/dist/types/control-types.d.ts +8 -0
- package/dist/types/control-types.d.ts.map +1 -1
- package/dist/types/equity-widget.d.ts +34 -0
- package/dist/types/equity-widget.d.ts.map +1 -0
- package/dist/types/heatmap-grid.d.ts +88 -0
- package/dist/types/heatmap-grid.d.ts.map +1 -0
- package/dist/types/index.d.ts +31 -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/optimization-heatmap-widget.d.ts +45 -0
- package/dist/types/optimization-heatmap-widget.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/option-smile-widget.d.ts +39 -0
- package/dist/types/option-smile-widget.d.ts.map +1 -0
- package/dist/types/pnl-curve.d.ts +46 -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/surface-grid.d.ts +64 -0
- package/dist/types/surface-grid.d.ts.map +1 -0
- package/dist/types/surface-widget.d.ts +64 -0
- package/dist/types/surface-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 +54 -3
- package/screenshots/equity.png +0 -0
- package/screenshots/log-monitor.png +0 -0
- package/screenshots/optimization-surface.png +0 -0
- package/screenshots/optimization.png +0 -0
- package/screenshots/option-desk.png +0 -0
- package/screenshots/option-smile.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/chart-engine.global.ts +64 -0
- package/src/chart-engine.ts +29 -0
- package/src/control-types.ts +8 -0
- package/src/equity-widget.ts +289 -0
- package/src/heatmap-grid.ts +405 -0
- package/src/index.ts +87 -0
- package/src/log-monitor-widget.ts +312 -0
- package/src/log-tree.ts +131 -0
- package/src/optimization-heatmap-widget.ts +347 -0
- package/src/option-desk-widget.ts +422 -0
- package/src/option-smile-widget.ts +281 -0
- package/src/pnl-curve.ts +216 -0
- package/src/statistics-widget.ts +226 -0
- package/src/strategies-widget.ts +435 -0
- package/src/surface-grid.ts +410 -0
- package/src/surface-widget.ts +457 -0
- package/src/trading-data.ts +22 -0
- package/styles/trading-controls.css +637 -0
- package/translation-keys.json +83 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Optimisation heatmap - multi-instance.
|
|
2
|
+
//
|
|
3
|
+
// One metric over two parameters: an axis of discrete values across, another up, a cell per pair
|
|
4
|
+
// of them, coloured by what that pair measured. The panel does not know what an optimisation is -
|
|
5
|
+
// a heatmap of a metric over two parameters is the same object whatever produced it - so the
|
|
6
|
+
// pairs are handed in and the two axis names come with them.
|
|
7
|
+
//
|
|
8
|
+
// The map is a <canvas>, the one surface a class name cannot reach: its geometry comes out of
|
|
9
|
+
// `heatmap-grid.ts` as numbers and its colours out of the host's `canvasPalette()`, so nothing
|
|
10
|
+
// here is painted from a palette this package chose. The tooltip is DOM, because it is text.
|
|
11
|
+
//
|
|
12
|
+
// Two things are drawn rather than tabulated and are worth stating. A cell carries no printed
|
|
13
|
+
// number: at the size a forty by forty sweep leaves a cell, digits are unreadable, and reading
|
|
14
|
+
// colour at that size is the entire reason to draw the map. And a pair nobody ran is struck
|
|
15
|
+
// through rather than left blank, because an unrun pair and a pair that measured zero are
|
|
16
|
+
// different facts that a scale with zero at its neutral point would otherwise paint the same.
|
|
17
|
+
import { formatStatistic } from './statistics-widget.js';
|
|
18
|
+
import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
|
|
19
|
+
import { ControlTypes } from './control-types.js';
|
|
20
|
+
import { assertHost } from './trading-host.js';
|
|
21
|
+
import { hitHeatmap, layoutHeatmap } from './heatmap-grid.js';
|
|
22
|
+
// Opacity, not colour. Every hue below is the host's `canvasPalette`; how solid a thing is drawn
|
|
23
|
+
// is the map's own business, and keeping the two apart is what lets one palette serve a light
|
|
24
|
+
// theme and a dark one without the package knowing which it is in.
|
|
25
|
+
//
|
|
26
|
+
// The ground is drawn under every measured cell so that a cell sitting exactly on the anchor -
|
|
27
|
+
// which takes no tint at all - is still visibly a cell. That is also what lets the tint itself
|
|
28
|
+
// go all the way down to nothing without a floor: neutral reads as the ground, and a pair that
|
|
29
|
+
// was never run has no ground under it.
|
|
30
|
+
const GROUND_ALPHA = 0.1;
|
|
31
|
+
const GAP_ALPHA = 0.25;
|
|
32
|
+
const LABEL_ALPHA = 0.65;
|
|
33
|
+
const BEST_WIDTH = 2;
|
|
34
|
+
// Gap between the cursor and the tooltip, and between the tooltip and the edge it is clamped
|
|
35
|
+
// against.
|
|
36
|
+
const TOOLTIP_OFFSET = 12;
|
|
37
|
+
const TOOLTIP_MARGIN = 4;
|
|
38
|
+
export class OptimizationHeatmapWidget {
|
|
39
|
+
static create(hostEl, state, deps) {
|
|
40
|
+
// Assert before building: the markup below is localized through the host, so a missing
|
|
41
|
+
// host has to fail here rather than render a panel captioned with raw English keys.
|
|
42
|
+
const host = assertHost(deps?.host, 'OptimizationHeatmapWidget');
|
|
43
|
+
const root = OptimizationHeatmapWidget._buildRoot(host);
|
|
44
|
+
root.id = makePanelId(OptimizationHeatmapWidget.TYPE);
|
|
45
|
+
hostEl.appendChild(root);
|
|
46
|
+
return new OptimizationHeatmapWidget(root, state || {}, deps);
|
|
47
|
+
}
|
|
48
|
+
// The panel's markup. The metric's name sits in the header rather than on the canvas: it is
|
|
49
|
+
// the caption the drawn legend is read against, and putting it in the chrome costs the map
|
|
50
|
+
// no pixels and leaves the text selectable and sized by the stylesheet.
|
|
51
|
+
static _buildRoot(host) {
|
|
52
|
+
const title = host.t('OptimizationHeatmap');
|
|
53
|
+
return makePanelRoot('optimization-heatmap-panel', title, [
|
|
54
|
+
makeElement('div', 'panel-header', {}, [
|
|
55
|
+
makeElement('span', '', {}, [title]),
|
|
56
|
+
makeElement('span', 'heatmap-metric', {}, []),
|
|
57
|
+
makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
|
|
58
|
+
]),
|
|
59
|
+
makeElement('div', 'panel-body', {}, [
|
|
60
|
+
makeElement('canvas', 'heatmap-canvas', { role: 'img', 'aria-label': host.t('OptimizationHeatmapChart') }, []),
|
|
61
|
+
makeElement('div', 'heatmap-empty', { hidden: '' }, [host.t('NoOptimizationResults')]),
|
|
62
|
+
makeElement('div', 'heatmap-tooltip', { role: 'tooltip', hidden: '' }, []),
|
|
63
|
+
]),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
constructor(rootEl, _state, deps) {
|
|
67
|
+
this._host = assertHost(deps?.host, 'OptimizationHeatmapWidget');
|
|
68
|
+
this.rootEl = rootEl;
|
|
69
|
+
this.canvasEl = this.rootEl.querySelector('.heatmap-canvas');
|
|
70
|
+
this._metricEl = this.rootEl.querySelector('.heatmap-metric');
|
|
71
|
+
this._emptyEl = this.rootEl.querySelector('.heatmap-empty');
|
|
72
|
+
this._tooltipEl = this.rootEl.querySelector('.heatmap-tooltip');
|
|
73
|
+
this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
|
|
74
|
+
this._ctx = this.canvasEl?.getContext('2d') ?? null;
|
|
75
|
+
this._data = null;
|
|
76
|
+
this._layout = null;
|
|
77
|
+
this._size = null;
|
|
78
|
+
this._resizeObserver = null;
|
|
79
|
+
this._closeBtn?.addEventListener('click', (e) => { e.preventDefault(); this._host.close(); });
|
|
80
|
+
this._bindHover();
|
|
81
|
+
this._setEmpty(true);
|
|
82
|
+
if (this.canvasEl && typeof ResizeObserver === 'function') {
|
|
83
|
+
this._resizeObserver = new ResizeObserver(() => this._render());
|
|
84
|
+
this._resizeObserver.observe(this.canvasEl);
|
|
85
|
+
}
|
|
86
|
+
this._host.register(this);
|
|
87
|
+
}
|
|
88
|
+
dispose() {
|
|
89
|
+
try {
|
|
90
|
+
this._resizeObserver?.disconnect();
|
|
91
|
+
}
|
|
92
|
+
catch { /* already torn down */ }
|
|
93
|
+
this._host.unregister(this);
|
|
94
|
+
try {
|
|
95
|
+
this.rootEl.remove();
|
|
96
|
+
}
|
|
97
|
+
catch { /* already detached */ }
|
|
98
|
+
}
|
|
99
|
+
/// Show this map. The whole of it at once: a pair that has dropped out of the set has stopped
|
|
100
|
+
/// existing, and leaving its cell behind would report a run that is no longer in the report.
|
|
101
|
+
update(data) {
|
|
102
|
+
this._data = data;
|
|
103
|
+
if (this._metricEl !== null)
|
|
104
|
+
this._metricEl.textContent = data.metricLabel;
|
|
105
|
+
this._hideTooltip();
|
|
106
|
+
this._render();
|
|
107
|
+
}
|
|
108
|
+
// The canvas in CSS pixels, and its backing store in device pixels. Without the second the
|
|
109
|
+
// map is drawn at a third of the resolution the screen has, and a lattice of hairlines is
|
|
110
|
+
// exactly the drawing that shows it.
|
|
111
|
+
_sizeCanvas() {
|
|
112
|
+
const canvas = this.canvasEl;
|
|
113
|
+
if (!canvas)
|
|
114
|
+
return;
|
|
115
|
+
const rect = canvas.getBoundingClientRect();
|
|
116
|
+
const ratio = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
117
|
+
const width = Math.max(1, Math.floor(rect.width));
|
|
118
|
+
const height = Math.max(1, Math.floor(rect.height));
|
|
119
|
+
if (canvas.width !== width * ratio || canvas.height !== height * ratio) {
|
|
120
|
+
canvas.width = width * ratio;
|
|
121
|
+
canvas.height = height * ratio;
|
|
122
|
+
// Everything below draws in CSS pixels, which is also the space a mouse event
|
|
123
|
+
// arrives in - so the hit test needs no conversion of its own.
|
|
124
|
+
this._ctx?.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
125
|
+
}
|
|
126
|
+
this._size = { width, height };
|
|
127
|
+
}
|
|
128
|
+
_render() {
|
|
129
|
+
const ctx = this._ctx;
|
|
130
|
+
if (ctx === null)
|
|
131
|
+
return;
|
|
132
|
+
this._sizeCanvas();
|
|
133
|
+
const size = this._size;
|
|
134
|
+
if (size === null)
|
|
135
|
+
return;
|
|
136
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
137
|
+
this._layout = null;
|
|
138
|
+
const data = this._data;
|
|
139
|
+
if (data === null) {
|
|
140
|
+
this._setEmpty(true);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const layout = layoutHeatmap({
|
|
144
|
+
width: size.width,
|
|
145
|
+
height: size.height,
|
|
146
|
+
cells: data.cells,
|
|
147
|
+
betterWhen: data.betterWhen,
|
|
148
|
+
xLabel: data.xLabel,
|
|
149
|
+
yLabel: data.yLabel,
|
|
150
|
+
});
|
|
151
|
+
this._setEmpty(layout === null);
|
|
152
|
+
if (layout === null)
|
|
153
|
+
return;
|
|
154
|
+
this._layout = layout;
|
|
155
|
+
const palette = this._host.presentation.canvasPalette();
|
|
156
|
+
ctx.font = palette.font;
|
|
157
|
+
ctx.lineWidth = 1;
|
|
158
|
+
for (const shape of layout.cells)
|
|
159
|
+
this._paintTint(ctx, shape.rect, shape.tint, palette.grid, palette.up, palette.down);
|
|
160
|
+
// A pair nobody ran, struck through. One diagonal rather than a fill of any kind: no
|
|
161
|
+
// amount of colour can say "not measured" on a map whose colours are all measurements.
|
|
162
|
+
ctx.globalAlpha = GAP_ALPHA;
|
|
163
|
+
ctx.strokeStyle = palette.grid;
|
|
164
|
+
for (const gap of layout.gaps) {
|
|
165
|
+
ctx.beginPath();
|
|
166
|
+
ctx.moveTo(gap.rect.x, gap.rect.y + gap.rect.height);
|
|
167
|
+
ctx.lineTo(gap.rect.x + gap.rect.width, gap.rect.y);
|
|
168
|
+
ctx.stroke();
|
|
169
|
+
}
|
|
170
|
+
// Last, so it sits over its own fill. In the grid colour because that is the only
|
|
171
|
+
// non-directional colour the palette carries, and an outline in either direction colour
|
|
172
|
+
// would vanish on half the cells it might land on.
|
|
173
|
+
const best = layout.cells.find(shape => shape.best);
|
|
174
|
+
if (best !== undefined) {
|
|
175
|
+
ctx.globalAlpha = 1;
|
|
176
|
+
ctx.strokeStyle = palette.grid;
|
|
177
|
+
ctx.lineWidth = BEST_WIDTH;
|
|
178
|
+
OptimizationHeatmapWidget._outline(ctx, best.rect, BEST_WIDTH / 2);
|
|
179
|
+
ctx.lineWidth = 1;
|
|
180
|
+
}
|
|
181
|
+
// The key, drawn out of the same two colours the map is: a legend in CSS would read the
|
|
182
|
+
// page's own tokens, and a host whose canvas palette comes from anywhere else would then
|
|
183
|
+
// explain its map in colours the map does not use.
|
|
184
|
+
for (const step of layout.legend.steps)
|
|
185
|
+
this._paintTint(ctx, step.rect, step.tint, palette.grid, palette.up, palette.down);
|
|
186
|
+
ctx.globalAlpha = LABEL_ALPHA;
|
|
187
|
+
ctx.fillStyle = palette.grid;
|
|
188
|
+
ctx.textBaseline = 'top';
|
|
189
|
+
ctx.textAlign = 'center';
|
|
190
|
+
for (const tick of layout.xTicks)
|
|
191
|
+
ctx.fillText(tick.text, tick.x, tick.y);
|
|
192
|
+
ctx.fillText(layout.xTitle.text, layout.xTitle.x, layout.xTitle.y);
|
|
193
|
+
ctx.fillText(formatStatistic(layout.legend.anchor.value), layout.legend.anchor.x, layout.legend.anchor.y);
|
|
194
|
+
ctx.textAlign = 'left';
|
|
195
|
+
ctx.fillText(layout.yTitle.text, layout.yTitle.x, layout.yTitle.y);
|
|
196
|
+
ctx.fillText(formatStatistic(layout.legend.low.value), layout.legend.low.x, layout.legend.low.y);
|
|
197
|
+
ctx.textAlign = 'right';
|
|
198
|
+
ctx.fillText(formatStatistic(layout.legend.high.value), layout.legend.high.x, layout.legend.high.y);
|
|
199
|
+
ctx.textBaseline = 'middle';
|
|
200
|
+
for (const tick of layout.yTicks)
|
|
201
|
+
ctx.fillText(tick.text, tick.x, tick.y);
|
|
202
|
+
ctx.globalAlpha = 1;
|
|
203
|
+
}
|
|
204
|
+
// The ground, then as much of the direction colour as the cell earned. Two fills rather than
|
|
205
|
+
// one blended colour: blending would need a background this control is not allowed to know.
|
|
206
|
+
_paintTint(ctx, rect, tint, ground, up, down) {
|
|
207
|
+
ctx.globalAlpha = GROUND_ALPHA;
|
|
208
|
+
ctx.fillStyle = ground;
|
|
209
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
210
|
+
if (tint === 0)
|
|
211
|
+
return;
|
|
212
|
+
ctx.globalAlpha = Math.abs(tint);
|
|
213
|
+
ctx.fillStyle = tint > 0 ? up : down;
|
|
214
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
215
|
+
}
|
|
216
|
+
static _outline(ctx, rect, inset) {
|
|
217
|
+
const left = rect.x + inset;
|
|
218
|
+
const top = rect.y + inset;
|
|
219
|
+
const right = rect.x + rect.width - inset;
|
|
220
|
+
const bottom = rect.y + rect.height - inset;
|
|
221
|
+
ctx.beginPath();
|
|
222
|
+
ctx.moveTo(left, top);
|
|
223
|
+
ctx.lineTo(right, top);
|
|
224
|
+
ctx.lineTo(right, bottom);
|
|
225
|
+
ctx.lineTo(left, bottom);
|
|
226
|
+
ctx.closePath();
|
|
227
|
+
ctx.stroke();
|
|
228
|
+
}
|
|
229
|
+
_setEmpty(empty) {
|
|
230
|
+
if (this._emptyEl === null)
|
|
231
|
+
return;
|
|
232
|
+
if (empty)
|
|
233
|
+
this._emptyEl.removeAttribute('hidden');
|
|
234
|
+
else
|
|
235
|
+
this._emptyEl.setAttribute('hidden', '');
|
|
236
|
+
}
|
|
237
|
+
_bindHover() {
|
|
238
|
+
const canvas = this.canvasEl;
|
|
239
|
+
if (!canvas)
|
|
240
|
+
return;
|
|
241
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
242
|
+
const layout = this._layout;
|
|
243
|
+
if (layout === null) {
|
|
244
|
+
this._hideTooltip();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const rect = canvas.getBoundingClientRect();
|
|
248
|
+
const x = e.clientX - rect.left;
|
|
249
|
+
const y = e.clientY - rect.top;
|
|
250
|
+
const hit = hitHeatmap(layout, x, y);
|
|
251
|
+
// A pair nobody ran says nothing, so it says nothing: a tooltip reading "no data"
|
|
252
|
+
// over a cell that is visibly struck through is the same statement twice.
|
|
253
|
+
if (hit !== null)
|
|
254
|
+
this._showTooltip(hit, x, y);
|
|
255
|
+
else
|
|
256
|
+
this._hideTooltip();
|
|
257
|
+
});
|
|
258
|
+
canvas.addEventListener('mouseleave', () => this._hideTooltip());
|
|
259
|
+
}
|
|
260
|
+
_showTooltip(shape, x, y) {
|
|
261
|
+
const tooltip = this._tooltipEl;
|
|
262
|
+
const canvas = this.canvasEl;
|
|
263
|
+
const data = this._data;
|
|
264
|
+
if (!tooltip || !canvas || data === null)
|
|
265
|
+
return;
|
|
266
|
+
const lines = [
|
|
267
|
+
this._tooltipLine(data.xLabel, shape.bucket.x),
|
|
268
|
+
this._tooltipLine(data.yLabel, shape.bucket.y),
|
|
269
|
+
this._tooltipLine(data.metricLabel, formatStatistic(shape.bucket.value)),
|
|
270
|
+
];
|
|
271
|
+
// How many runs the figure is the mean of, and only when it is a mean of more than one.
|
|
272
|
+
// A count of 1 on every cell is a column of noise; a count of 5 on one of them is the
|
|
273
|
+
// difference between a result and a coincidence.
|
|
274
|
+
if (shape.bucket.count > 1)
|
|
275
|
+
lines.push(this._tooltipLine(this._host.t('Runs'), String(shape.bucket.count)));
|
|
276
|
+
if (shape.best)
|
|
277
|
+
lines.push(makeElement('div', 'heatmap-tt-row heatmap-tt-best', {}, [this._host.t('Best')]));
|
|
278
|
+
tooltip.replaceChildren(...lines);
|
|
279
|
+
// Measured, then placed: the size is only known once it is showing, and it is clamped
|
|
280
|
+
// inside the canvas so a cell near the right or bottom edge does not push its own
|
|
281
|
+
// tooltip out of view.
|
|
282
|
+
tooltip.removeAttribute('hidden');
|
|
283
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
284
|
+
const parentRect = tooltip.offsetParent?.getBoundingClientRect() ?? canvasRect;
|
|
285
|
+
const originX = canvasRect.left - parentRect.left;
|
|
286
|
+
const originY = canvasRect.top - parentRect.top;
|
|
287
|
+
const maxX = originX + canvasRect.width - tooltip.offsetWidth - TOOLTIP_MARGIN;
|
|
288
|
+
const maxY = originY + canvasRect.height - tooltip.offsetHeight - TOOLTIP_MARGIN;
|
|
289
|
+
tooltip.style.left = `${Math.max(0, Math.min(originX + x + TOOLTIP_OFFSET, maxX))}px`;
|
|
290
|
+
tooltip.style.top = `${Math.max(0, Math.min(originY + y + TOOLTIP_OFFSET, maxY))}px`;
|
|
291
|
+
}
|
|
292
|
+
_tooltipLine(label, value) {
|
|
293
|
+
return makeElement('div', 'heatmap-tt-row', {}, [
|
|
294
|
+
makeElement('span', 'heatmap-tt-label', {}, [label]),
|
|
295
|
+
makeElement('span', 'heatmap-tt-value', {}, [value]),
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
_hideTooltip() {
|
|
299
|
+
this._tooltipEl?.setAttribute('hidden', '');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
OptimizationHeatmapWidget.TYPE = ControlTypes.OptimizationHeatmap;
|
|
303
|
+
//# sourceMappingURL=optimization-heatmap-widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimization-heatmap-widget.js","sourceRoot":"","sources":["../../src/optimization-heatmap-widget.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,6DAA6D;AAC7D,EAAE;AACF,8FAA8F;AAC9F,+FAA+F;AAC/F,6FAA6F;AAC7F,EAAE;AACF,8FAA8F;AAC9F,+FAA+F;AAC/F,4FAA4F;AAC5F,0FAA0F;AAC1F,8FAA8F;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAe,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAsB9D,iGAAiG;AACjG,8FAA8F;AAC9F,mEAAmE;AACnE,EAAE;AACF,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,wCAAwC;AACxC,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,WAAW,GAAG,IAAI,CAAC;AACzB,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,6FAA6F;AAC7F,WAAW;AACX,MAAM,cAAc,GAAG,EAAE,CAAC;AAC1B,MAAM,cAAc,GAAG,CAAC,CAAC;AAEzB,MAAM,OAAO,yBAAyB;IAmBlC,MAAM,CAAC,MAAM,CAAC,MAAmB,EAAE,KAA8B,EAAE,IAA6B;QAC5F,uFAAuF;QACvF,oFAAoF;QACpF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,2BAA2B,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,yBAAyB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,CAAC,EAAE,GAAG,WAAW,CAAC,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACzB,OAAO,IAAI,yBAAyB,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,4FAA4F;IAC5F,2FAA2F;IAC3F,wEAAwE;IACxE,MAAM,CAAC,UAAU,CAAC,IAAiB;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;QAC5C,OAAO,aAAa,CAAC,4BAA4B,EAAE,KAAK,EAAE;YACtD,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE;gBACnC,WAAW,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;gBACpC,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,EAAE,EAAE,CAAC;gBAC7C,cAAc,CAAC,4CAA4C,EAAE,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;aACjH,CAAC;YACF,WAAW,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE;gBACjC,WAAW,CAAC,QAAQ,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,0BAA0B,CAAC,EAAE,EAAE,EAAE,CAAC;gBAC9G,WAAW,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC;gBACtF,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;aAC7E,CAAC;SACL,CAAC,CAAC;IACP,CAAC;IAED,YAAY,MAAmB,EAAE,MAA+B,EAAE,IAA6B;QAC3F,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,2BAA2B,CAAC,CAAC;QAEjE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;QAC7D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;QAC9D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC;QAC5D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;QAChE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;QAC/D,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;QACpD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAE5B,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9F,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAErB,IAAI,IAAI,CAAC,QAAQ,IAAI,OAAO,cAAc,KAAK,UAAU,EAAE,CAAC;YACxD,IAAI,CAAC,eAAe,GAAG,IAAI,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;YAChE,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChD,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO;QACH,IAAI,CAAC;YAAC,IAAI,CAAC,eAAe,EAAE,UAAU,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,uBAAuB,CAAC,CAAC;QAC7E,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC;YAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC;IAClE,CAAC;IAED,8FAA8F;IAC9F,6FAA6F;IAC7F,MAAM,CAAC,IAAiB;QACpB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI;YAAE,IAAI,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QAC3E,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,2FAA2F;IAC3F,0FAA0F;IAC1F,qCAAqC;IACrC,WAAW;QACP,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,OAAO,gBAAgB,KAAK,QAAQ,IAAI,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;QAClG,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QACpD,IAAI,MAAM,CAAC,KAAK,KAAK,KAAK,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,GAAG,KAAK,EAAE,CAAC;YACrE,MAAM,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;YAC7B,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;YAC/B,8EAA8E;YAC9E,+DAA+D;YAC/D,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACnC,CAAC;IAED,OAAO;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QACzB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAE1B,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAEpB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;YAAC,OAAO;QAAC,CAAC;QAEpD,MAAM,MAAM,GAAG,aAAa,CAAC;YACzB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;SACtB,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;QAChC,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO;QAC5B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QAEtB,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC;QACxD,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACxB,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC;QAElB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK;YAAE,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAEvH,qFAAqF;QACrF,uFAAuF;QACvF,GAAG,CAAC,WAAW,GAAG,SAAS,CAAC;QAC5B,GAAG,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5B,GAAG,CAAC,SAAS,EAAE,CAAC;YAChB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACrD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpD,GAAG,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC;QAED,kFAAkF;QAClF,wFAAwF;QACxF,mDAAmD;QACnD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC;YACpB,GAAG,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;YAC/B,GAAG,CAAC,SAAS,GAAG,UAAU,CAAC;YAC3B,yBAAyB,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;YACnE,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,CAAC;QAED,wFAAwF;QACxF,yFAAyF;QACzF,mDAAmD;QACnD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK;YAClC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAEvF,GAAG,CAAC,WAAW,GAAG,WAAW,CAAC;QAC9B,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;QAE7B,GAAG,CAAC,YAAY,GAAG,KAAK,CAAC;QACzB,GAAG,CAAC,SAAS,GAAG,QAAQ,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM;YAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1E,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnE,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAE1G,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC;QACvB,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnE,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAEjG,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC;QACxB,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAEpG,GAAG,CAAC,YAAY,GAAG,QAAQ,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM;YAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAE1E,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC;IACxB,CAAC;IAED,6FAA6F;IAC7F,4FAA4F;IAC5F,UAAU,CAAC,GAA6B,EAAE,IAAc,EAAE,IAAY,EAAE,MAAc,EAAE,EAAU,EAAE,IAAY;QAC5G,GAAG,CAAC,WAAW,GAAG,YAAY,CAAC;QAC/B,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC;QACvB,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACtD,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO;QAEvB,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,GAAG,CAAC,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACrC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,GAA6B,EAAE,IAAc,EAAE,KAAa;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QAC5C,GAAG,CAAC,SAAS,EAAE,CAAC;QAChB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACtB,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACvB,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC1B,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzB,GAAG,CAAC,SAAS,EAAE,CAAC;QAChB,GAAG,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC;IAED,SAAS,CAAC,KAAc;QACpB,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAO;QACnC,IAAI,KAAK;YAAE,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;;YAC9C,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,UAAU;QACN,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,CAAC,CAAa,EAAE,EAAE;YACnD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;YAC5B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBAAC,IAAI,CAAC,YAAY,EAAE,CAAC;gBAAC,OAAO;YAAC,CAAC;YACrD,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAC;YAC5C,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;YAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACrC,kFAAkF;YAClF,0EAA0E;YAC1E,IAAI,GAAG,KAAK,IAAI;gBAAE,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;;gBAC1C,IAAI,CAAC,YAAY,EAAE,CAAC;QAC7B,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,YAAY,CAAC,KAAoB,EAAE,CAAS,EAAE,CAAS;QACnD,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO;QAEjD,MAAM,KAAK,GAAkB;YACzB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC9C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC9C,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;SAC3E,CAAC;QACF,wFAAwF;QACxF,sFAAsF;QACtF,iDAAiD;QACjD,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACpF,IAAI,KAAK,CAAC,IAAI;YACV,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,gCAAgC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACjG,OAAO,CAAC,eAAe,CAAC,GAAG,KAAK,CAAC,CAAC;QAElC,sFAAsF;QACtF,kFAAkF;QAClF,uBAAuB;QACvB,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,UAAU,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAC;QAClD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,EAAE,qBAAqB,EAAE,IAAI,UAAU,CAAC;QAC/E,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC;QAClD,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC;QAChD,MAAM,IAAI,GAAG,OAAO,GAAG,UAAU,CAAC,KAAK,GAAG,OAAO,CAAC,WAAW,GAAG,cAAc,CAAC;QAC/E,MAAM,IAAI,GAAG,OAAO,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,cAAc,CAAC;QACjF,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,GAAG,cAAc,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;QACtF,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,GAAG,cAAc,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;IACzF,CAAC;IAED,YAAY,CAAC,KAAa,EAAE,KAAa;QACrC,OAAO,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE;YAC5C,WAAW,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;YACpD,WAAW,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;SACvD,CAAC,CAAC;IACP,CAAC;IAED,YAAY;QACR,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;;AA7RM,8BAAI,GAAG,YAAY,CAAC,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// Option desk — multi-instance.
|
|
2
|
+
//
|
|
3
|
+
// One expiry of a chain, strike by strike: the call's side on the left, the put's mirrored on
|
|
4
|
+
// the right, and the strike between them. Every figure a trader compares across the strip -
|
|
5
|
+
// volume, open interest, volatility - carries a bar as well as a number, because a chain is
|
|
6
|
+
// read by shape first and by value second.
|
|
7
|
+
//
|
|
8
|
+
// Two scales, and the difference matters. Volume and open interest are scaled per side, since
|
|
9
|
+
// calls and puts trade in different sizes and comparing a call's volume against the busiest put
|
|
10
|
+
// says nothing. Volatility is scaled across BOTH sides at once - a skew is exactly the
|
|
11
|
+
// comparison between them, and two independent scales would flatten it.
|
|
12
|
+
//
|
|
13
|
+
// Greeks arrive one of two ways. A host that computes them sends them and the desk shows what
|
|
14
|
+
// it was given; a host that sends volatility instead has them computed here, from the same
|
|
15
|
+
// Black-Scholes the desktop uses. Neither is a fallback for the other going wrong - they are
|
|
16
|
+
// two shapes of host, and which one a row came from is not the desk's business.
|
|
17
|
+
import { formatPrice, formatQty } from './formatters.js';
|
|
18
|
+
import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
|
|
19
|
+
import { ControlTypes } from './control-types.js';
|
|
20
|
+
import { makeGridMenu } from './grid-menu.js';
|
|
21
|
+
import { assertHost } from './trading-host.js';
|
|
22
|
+
import { OptionTypes, greeks as computeGreeks } from './black-scholes.js';
|
|
23
|
+
import { DataGrid } from '@stocksharp/grids/data-grid';
|
|
24
|
+
export class OptionDeskWidget {
|
|
25
|
+
static create(hostEl, state, deps) {
|
|
26
|
+
const host = assertHost(deps?.host, 'OptionDeskWidget');
|
|
27
|
+
const root = OptionDeskWidget._buildRoot(host);
|
|
28
|
+
root.id = makePanelId(OptionDeskWidget.TYPE);
|
|
29
|
+
hostEl.appendChild(root);
|
|
30
|
+
return new OptionDeskWidget(root, state || {}, deps);
|
|
31
|
+
}
|
|
32
|
+
static _buildRoot(host) {
|
|
33
|
+
const title = host.t('OptionDesk');
|
|
34
|
+
return makePanelRoot('option-desk-panel', title, [
|
|
35
|
+
makeElement('div', 'panel-header', {}, [
|
|
36
|
+
makeElement('span', '', {}, [title]),
|
|
37
|
+
makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
|
|
38
|
+
]),
|
|
39
|
+
makeElement('div', 'panel-body panel-body-with-rail', {}, [
|
|
40
|
+
makeElement('div', 'panel-body-content', {}, [
|
|
41
|
+
makeElement('table', 'terminal-table option-desk-table', { role: 'table', 'aria-label': host.t('OptionChain') }, [
|
|
42
|
+
makeElement('thead', '', {}, []),
|
|
43
|
+
makeElement('tbody', 'option-desk-body', {}, []),
|
|
44
|
+
]),
|
|
45
|
+
]),
|
|
46
|
+
makeElement('div', 'panel-rail', { role: 'toolbar', 'aria-label': host.t('OptionDeskActions') }, [
|
|
47
|
+
makeIconButton('bt-icon-btn panel-export-btn', host.t('ExportToExcel'), 'bi-file-earmark-spreadsheet', {}),
|
|
48
|
+
]),
|
|
49
|
+
]),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
constructor(rootEl, _state, deps) {
|
|
53
|
+
this._host = assertHost(deps?.host, 'OptionDeskWidget');
|
|
54
|
+
this.rootEl = rootEl;
|
|
55
|
+
this.el = this.rootEl.querySelector('.option-desk-body');
|
|
56
|
+
this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
|
|
57
|
+
this._exportBtn = this.rootEl.querySelector('.panel-export-btn');
|
|
58
|
+
this._rows = [];
|
|
59
|
+
this._context = {};
|
|
60
|
+
this._places = greekScales([], {});
|
|
61
|
+
this._closeBtn?.addEventListener('click', (e) => { e.preventDefault(); this._host.close(); });
|
|
62
|
+
this._exportBtn?.addEventListener('click', (e) => { e.preventDefault(); this._export(); });
|
|
63
|
+
const head = this.rootEl.querySelector('.option-desk-table thead');
|
|
64
|
+
this._grid = head && this.el
|
|
65
|
+
? new DataGrid({
|
|
66
|
+
head: head,
|
|
67
|
+
body: this.el,
|
|
68
|
+
columns: this._columns(),
|
|
69
|
+
// By strike, ascending. A chain has exactly one order and it is not negotiable:
|
|
70
|
+
// the strip is read as a ladder, and re-sorting it by any column destroys that.
|
|
71
|
+
defaultSort: { col: 'strike', dir: 'asc' },
|
|
72
|
+
rowKey: (r) => String(r.strike),
|
|
73
|
+
emptyText: this._host.t('NoOptions'),
|
|
74
|
+
rowClass: (r) => this._rowClass(r),
|
|
75
|
+
contextMenu: makeGridMenu(this._host),
|
|
76
|
+
selection: 'multi',
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
79
|
+
// The greeks and the far volatilities are there when wanted and out of the way when not:
|
|
80
|
+
// a desk is read across, and thirty-six columns at once cannot be.
|
|
81
|
+
this._grid?.setState({
|
|
82
|
+
hidden: [
|
|
83
|
+
'callRho', 'callTheta', 'callHv', 'callTheor',
|
|
84
|
+
'putRho', 'putTheta', 'putHv', 'putTheor',
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
this._host.register(this);
|
|
88
|
+
}
|
|
89
|
+
dispose() {
|
|
90
|
+
this._grid?.destroy();
|
|
91
|
+
this._host.unregister(this);
|
|
92
|
+
try {
|
|
93
|
+
this.rootEl.remove();
|
|
94
|
+
}
|
|
95
|
+
catch { /* already detached */ }
|
|
96
|
+
}
|
|
97
|
+
/// Show this chain, priced against this context.
|
|
98
|
+
///
|
|
99
|
+
/// Both together, always: a chain and the underlying it is priced off are one observation,
|
|
100
|
+
/// and refreshing them separately shows greeks computed from a price that has moved.
|
|
101
|
+
update(strikes, context = {}) {
|
|
102
|
+
this._context = context ?? {};
|
|
103
|
+
this._rows = scaleChain(strikes ?? [], this._context);
|
|
104
|
+
this._places = greekScales(this._rows, this._context);
|
|
105
|
+
this._grid?.setRows(this._rows);
|
|
106
|
+
}
|
|
107
|
+
/// The rows as the desk holds them, scales and intrinsic values resolved.
|
|
108
|
+
rows() {
|
|
109
|
+
return this._rows;
|
|
110
|
+
}
|
|
111
|
+
_export() {
|
|
112
|
+
this._grid?.download('option-chain', this._host.t('OptionDesk'));
|
|
113
|
+
}
|
|
114
|
+
// Which side of the money this strike is on. A chain is read from the money outwards, and
|
|
115
|
+
// the line between the two halves is what a reader finds first.
|
|
116
|
+
_rowClass(row) {
|
|
117
|
+
const asset = this._context.assetPrice;
|
|
118
|
+
if (asset === null || asset === undefined)
|
|
119
|
+
return 'option-row';
|
|
120
|
+
return `option-row ${row.strike < asset ? 'option-itm-call' : 'option-itm-put'}`;
|
|
121
|
+
}
|
|
122
|
+
_columns() {
|
|
123
|
+
const label = (key) => this._host.t(key);
|
|
124
|
+
const side = (which) => {
|
|
125
|
+
const prefix = which;
|
|
126
|
+
const at = (r) => r[which];
|
|
127
|
+
const g = (r) => sideGreeks(r, which, this._context);
|
|
128
|
+
return [
|
|
129
|
+
{ key: `${prefix}Rho`, header: label('Rho'), exportable: true, value: (r) => g(r)?.rho ?? null, render: (r) => decimals(g(r)?.rho, this._places.rho) },
|
|
130
|
+
{ key: `${prefix}Theta`, header: label('Theta'), exportable: true, value: (r) => g(r)?.theta ?? null, render: (r) => decimals(g(r)?.theta, this._places.theta) },
|
|
131
|
+
{ key: `${prefix}Vega`, header: label('Vega'), exportable: true, value: (r) => g(r)?.vega ?? null, render: (r) => decimals(g(r)?.vega, this._places.vega) },
|
|
132
|
+
{ key: `${prefix}Gamma`, header: label('Gamma'), exportable: true, value: (r) => g(r)?.gamma ?? null, render: (r) => decimals(g(r)?.gamma, this._places.gamma) },
|
|
133
|
+
{ key: `${prefix}Delta`, header: label('Delta'), exportable: true, value: (r) => g(r)?.delta ?? null, render: (r) => decimals(g(r)?.delta, this._places.delta) },
|
|
134
|
+
{ key: `${prefix}Bid`, header: label('Bid'), exportable: true, cellClass: () => 'option-bid', value: (r) => at(r).bid ?? null, render: (r) => price(at(r).bid) },
|
|
135
|
+
{ key: `${prefix}Ask`, header: label('Ask'), exportable: true, cellClass: () => 'option-ask', value: (r) => at(r).ask ?? null, render: (r) => price(at(r).ask) },
|
|
136
|
+
{ key: `${prefix}Theor`, header: label('TheorPrice'), exportable: true, value: (r) => at(r).theoretical ?? null, render: (r) => price(at(r).theoretical) },
|
|
137
|
+
{
|
|
138
|
+
key: `${prefix}Volume`, header: label('Volume'), exportable: true,
|
|
139
|
+
value: (r) => at(r).volume ?? 0,
|
|
140
|
+
render: (r) => this._bar(at(r).volume, which === 'call' ? r.maxCallVolume : r.maxPutVolume, which, formatQty(at(r).volume ?? 0)),
|
|
141
|
+
exportValue: (r) => at(r).volume ?? 0,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: `${prefix}Oi`, header: label('OI'), exportable: true,
|
|
145
|
+
value: (r) => at(r).openInterest ?? 0,
|
|
146
|
+
render: (r) => this._bar(at(r).openInterest, which === 'call' ? r.maxCallOpenInterest : r.maxPutOpenInterest, which, formatQty(at(r).openInterest ?? 0)),
|
|
147
|
+
exportValue: (r) => at(r).openInterest ?? 0,
|
|
148
|
+
},
|
|
149
|
+
{ key: `${prefix}Symbol`, header: which === 'call' ? label('Call') : label('Put'), exportable: true, value: (r) => at(r).symbol ?? '' },
|
|
150
|
+
{
|
|
151
|
+
key: `${prefix}IvBid`, header: label('IVBid'), exportable: true,
|
|
152
|
+
value: (r) => at(r).ivBid ?? null,
|
|
153
|
+
render: (r) => this._bar(at(r).ivBid, r.maxVolatility, 'iv', percent(at(r).ivBid)),
|
|
154
|
+
exportValue: (r) => at(r).ivBid ?? '',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: `${prefix}IvAsk`, header: label('IVAsk'), exportable: true,
|
|
158
|
+
value: (r) => at(r).ivAsk ?? null,
|
|
159
|
+
render: (r) => this._bar(at(r).ivAsk, r.maxVolatility, 'iv', percent(at(r).ivAsk)),
|
|
160
|
+
exportValue: (r) => at(r).ivAsk ?? '',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: `${prefix}IvLast`, header: label('IVLast'), exportable: true,
|
|
164
|
+
value: (r) => at(r).ivLast ?? null,
|
|
165
|
+
render: (r) => this._bar(at(r).ivLast, r.maxVolatility, 'iv', percent(at(r).ivLast)),
|
|
166
|
+
exportValue: (r) => at(r).ivLast ?? '',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: `${prefix}Hv`, header: label('HV'), exportable: true,
|
|
170
|
+
value: (r) => at(r).historicalVolatility ?? null,
|
|
171
|
+
render: (r) => this._bar(at(r).historicalVolatility, r.maxVolatility, 'iv', percent(at(r).historicalVolatility)),
|
|
172
|
+
exportValue: (r) => at(r).historicalVolatility ?? '',
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
};
|
|
176
|
+
// Declared outward from the strike: volatilities and quotes against the middle, where
|
|
177
|
+
// the two sides are compared, and the greeks out at the edges. The put side is that
|
|
178
|
+
// same order reflected, which is what makes the chain a mirror rather than a repeat.
|
|
179
|
+
const puts = side('put').slice().reverse();
|
|
180
|
+
return [
|
|
181
|
+
...side('call'),
|
|
182
|
+
{
|
|
183
|
+
key: 'strike',
|
|
184
|
+
header: label('Strike'),
|
|
185
|
+
exportable: true,
|
|
186
|
+
cellClass: () => 'option-strike',
|
|
187
|
+
value: (r) => r.strike,
|
|
188
|
+
render: (r) => price(r.strike),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: 'intrinsic',
|
|
192
|
+
header: label('IntrinsicValue'),
|
|
193
|
+
exportable: true,
|
|
194
|
+
cellClass: () => 'option-intrinsic',
|
|
195
|
+
// Whichever side is in the money at this strike; the other is worth nothing to
|
|
196
|
+
// exercise, and showing both would be one number and one zero on every row.
|
|
197
|
+
value: (r) => Math.max(r.callIntrinsic, r.putIntrinsic),
|
|
198
|
+
render: (r) => price(Math.max(r.callIntrinsic, r.putIntrinsic)),
|
|
199
|
+
},
|
|
200
|
+
...puts,
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
// A figure with its share of the strip behind it. The bar is a width, not a drawing: it
|
|
204
|
+
// scales with the cell, it prints, and it needs no canvas per row.
|
|
205
|
+
_bar(value, max, kind, text) {
|
|
206
|
+
if (value === null || value === undefined || !isFinite(value) || value <= 0)
|
|
207
|
+
return text;
|
|
208
|
+
const cell = document.createElement('span');
|
|
209
|
+
cell.className = 'option-bar-cell';
|
|
210
|
+
const bar = document.createElement('span');
|
|
211
|
+
bar.className = `option-bar option-bar-${kind}`;
|
|
212
|
+
bar.style.width = `${Math.min(100, (value / (max || 1)) * 100).toFixed(1)}%`;
|
|
213
|
+
cell.appendChild(bar);
|
|
214
|
+
const label = document.createElement('span');
|
|
215
|
+
label.className = 'option-bar-text';
|
|
216
|
+
label.textContent = text;
|
|
217
|
+
cell.appendChild(label);
|
|
218
|
+
return cell;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
OptionDeskWidget.TYPE = ControlTypes.OptionDesk;
|
|
222
|
+
/// Resolve the bar scales and the intrinsic values over a whole chain.
|
|
223
|
+
///
|
|
224
|
+
/// One pass over every strike, not one per row: a bar means "this much of the busiest strike",
|
|
225
|
+
/// and a scale computed per row would make every row its own maximum.
|
|
226
|
+
export function scaleChain(strikes, context) {
|
|
227
|
+
const asset = context.assetPrice ?? null;
|
|
228
|
+
const maxOf = (pick, sides) => Math.max(0, ...strikes.flatMap(r => sides(r).map(s => pick(s) ?? 0)).filter(v => isFinite(v)));
|
|
229
|
+
const maxCallVolume = maxOf(s => s.volume, r => [r.call]);
|
|
230
|
+
const maxPutVolume = maxOf(s => s.volume, r => [r.put]);
|
|
231
|
+
const maxCallOpenInterest = maxOf(s => s.openInterest, r => [r.call]);
|
|
232
|
+
const maxPutOpenInterest = maxOf(s => s.openInterest, r => [r.put]);
|
|
233
|
+
// One scale across both sides: the comparison a skew IS, is the one between them.
|
|
234
|
+
const maxVolatility = Math.max(maxOf(s => s.ivBid, r => [r.call, r.put]), maxOf(s => s.ivAsk, r => [r.call, r.put]), maxOf(s => s.ivLast, r => [r.call, r.put]), maxOf(s => s.historicalVolatility, r => [r.call, r.put]));
|
|
235
|
+
return strikes.map(row => ({
|
|
236
|
+
...row,
|
|
237
|
+
maxCallVolume,
|
|
238
|
+
maxPutVolume,
|
|
239
|
+
maxCallOpenInterest,
|
|
240
|
+
maxPutOpenInterest,
|
|
241
|
+
maxVolatility,
|
|
242
|
+
callIntrinsic: asset === null ? 0 : Math.max(0, asset - row.strike),
|
|
243
|
+
putIntrinsic: asset === null ? 0 : Math.max(0, row.strike - asset),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
/// The greeks for one side: the host's if it sent them, otherwise computed from its volatility.
|
|
247
|
+
///
|
|
248
|
+
/// Null when neither is possible - no volatility, or nothing to price against. A blank is the
|
|
249
|
+
/// honest answer there; a zero would read as a measured delta of nothing.
|
|
250
|
+
export function sideGreeks(row, which, context) {
|
|
251
|
+
const side = row[which];
|
|
252
|
+
if (side.greeks !== undefined)
|
|
253
|
+
return side.greeks;
|
|
254
|
+
const deviation = side.ivLast ?? side.ivBid ?? side.ivAsk ?? side.historicalVolatility ?? null;
|
|
255
|
+
const assetPrice = context.assetPrice ?? null;
|
|
256
|
+
const timeToExpiry = context.timeToExpiry ?? null;
|
|
257
|
+
if (deviation === null || assetPrice === null || timeToExpiry === null)
|
|
258
|
+
return null;
|
|
259
|
+
return computeGreeks(which === 'call' ? OptionTypes.Call : OptionTypes.Put, {
|
|
260
|
+
assetPrice,
|
|
261
|
+
strike: row.strike,
|
|
262
|
+
timeToExpiry,
|
|
263
|
+
riskFree: context.riskFree ?? 0,
|
|
264
|
+
dividend: context.dividend ?? 0,
|
|
265
|
+
deviation,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/// Every greek this desk can show. Order is the order a chain is read in, outwards from delta.
|
|
269
|
+
const GREEK_KEYS = ['delta', 'gamma', 'vega', 'theta', 'rho'];
|
|
270
|
+
/// Three significant digits for the smallest figure in a column, within bounds a column can hold.
|
|
271
|
+
const GREEK_DIGITS = 4;
|
|
272
|
+
const GREEK_MIN_PLACES = 2;
|
|
273
|
+
const GREEK_MAX_PLACES = 8;
|
|
274
|
+
/// How many decimal places a column of greeks needs.
|
|
275
|
+
///
|
|
276
|
+
/// One count cannot serve every greek: a delta is about one, while a gamma on an underlying at
|
|
277
|
+
/// 60000 is about 0.00003, and the four places that suit the first show every strike of the
|
|
278
|
+
/// second as 0.0000 - a column that is present, aligned, and says nothing. So the count comes
|
|
279
|
+
/// from the numbers, sized to the smallest of them.
|
|
280
|
+
///
|
|
281
|
+
/// One count for the whole column, not per cell: a column of figures is read down its decimal
|
|
282
|
+
/// point, and a ragged one is read a cell at a time.
|
|
283
|
+
export function greekPlaces(values) {
|
|
284
|
+
const scale = Math.min(...values
|
|
285
|
+
.filter((v) => typeof v === 'number' && isFinite(v) && v !== 0)
|
|
286
|
+
.map(Math.abs));
|
|
287
|
+
// Nothing measurable: a zero column is a zero column at any width.
|
|
288
|
+
if (!isFinite(scale))
|
|
289
|
+
return GREEK_MIN_PLACES;
|
|
290
|
+
const places = GREEK_DIGITS - 1 - Math.floor(Math.log10(scale));
|
|
291
|
+
return Math.min(GREEK_MAX_PLACES, Math.max(GREEK_MIN_PLACES, places));
|
|
292
|
+
}
|
|
293
|
+
/// The places for each greek, measured across both sides of the chain at once.
|
|
294
|
+
///
|
|
295
|
+
/// Across both sides deliberately: the desk mirrors, and a gamma written to eight places on the
|
|
296
|
+
/// left and five on the right stops being a mirror.
|
|
297
|
+
export function greekScales(rows, context) {
|
|
298
|
+
const all = rows
|
|
299
|
+
.flatMap(r => [sideGreeks(r, OptionTypes.Call, context), sideGreeks(r, OptionTypes.Put, context)])
|
|
300
|
+
.filter((g) => g !== null);
|
|
301
|
+
const places = {};
|
|
302
|
+
for (const key of GREEK_KEYS)
|
|
303
|
+
places[key] = greekPlaces(all.map(g => g[key]));
|
|
304
|
+
return places;
|
|
305
|
+
}
|
|
306
|
+
function decimals(value, places) {
|
|
307
|
+
if (value === null || value === undefined || !isFinite(value))
|
|
308
|
+
return '';
|
|
309
|
+
return value.toFixed(places);
|
|
310
|
+
}
|
|
311
|
+
function price(value) {
|
|
312
|
+
if (value === null || value === undefined || !isFinite(value))
|
|
313
|
+
return '';
|
|
314
|
+
return formatPrice(value);
|
|
315
|
+
}
|
|
316
|
+
/// A volatility, as the points a desk quotes it in rather than the fraction it is held as.
|
|
317
|
+
function percent(value) {
|
|
318
|
+
if (value === null || value === undefined || !isFinite(value))
|
|
319
|
+
return '';
|
|
320
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
321
|
+
}
|
|
322
|
+
//# sourceMappingURL=option-desk-widget.js.map
|