@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
package/src/control-types.ts
CHANGED
|
@@ -22,6 +22,14 @@ export const ControlTypes = {
|
|
|
22
22
|
OrderBook: 'orderbook',
|
|
23
23
|
TradeFeed: 'tradefeed',
|
|
24
24
|
OrderEntry: 'orderEntry',
|
|
25
|
+
Statistics: 'statistics',
|
|
26
|
+
LogMonitor: 'logMonitor',
|
|
27
|
+
Strategies: 'strategies',
|
|
28
|
+
OptionDesk: 'optionDesk',
|
|
29
|
+
OptionSmile: 'optionSmile',
|
|
30
|
+
Equity: 'equity',
|
|
31
|
+
OptimizationHeatmap: 'optimizationHeatmap',
|
|
32
|
+
OptimizationSurface: 'optimizationSurface',
|
|
25
33
|
} as const;
|
|
26
34
|
|
|
27
35
|
export type ControlType = typeof ControlTypes[keyof typeof ControlTypes];
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Equity curve — multi-instance.
|
|
2
|
+
//
|
|
3
|
+
// A run's cumulative P&L at the size of a chart, drawn by @stocksharp/chart. It used to be a
|
|
4
|
+
// canvas of its own with a crosshair, a legend and a zoom written here; all three are the
|
|
5
|
+
// engine's, better than the versions this file had, and an equity curve over time is exactly
|
|
6
|
+
// what a time-series engine is for.
|
|
7
|
+
//
|
|
8
|
+
// What is left here is the part that is this package's: turning a run into a series, wording the
|
|
9
|
+
// readout through the host, and taking the engine's colours from `presentation.canvasPalette()`
|
|
10
|
+
// so a curve in a panel matches the sparkline in a table cell beside it.
|
|
11
|
+
//
|
|
12
|
+
// The sparkline is NOT this. A cell forty pixels tall gets `pnl-curve.ts` and a canvas, because
|
|
13
|
+
// a chart engine per table row is absurd - the two draw the same run at sizes that want
|
|
14
|
+
// different machinery.
|
|
15
|
+
import { formatPnl } from './formatters.js';
|
|
16
|
+
import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
|
|
17
|
+
import { ControlTypes } from './control-types.js';
|
|
18
|
+
import { TradingHost, assertHost } from './trading-host.js';
|
|
19
|
+
import { type PnlPoint } from './pnl-curve.js';
|
|
20
|
+
import {
|
|
21
|
+
AreaSeries, CrosshairMode, createChart,
|
|
22
|
+
type AreaData, type CrosshairEvent, type IChartApi, type ISeriesApi, type Time,
|
|
23
|
+
} from './chart-engine.js';
|
|
24
|
+
|
|
25
|
+
/// What the panel needs beyond the host port. Nothing: there is nothing on a curve to act on.
|
|
26
|
+
export interface EquityDeps {
|
|
27
|
+
host: TradingHost;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// How solid the area under the curve is drawn. Not a colour - the two colours are the host's -
|
|
31
|
+
// but how far they are faded, which is this chart's own business.
|
|
32
|
+
const FILL_TOP_ALPHA = 0.28;
|
|
33
|
+
const FILL_BOTTOM_ALPHA = 0.02;
|
|
34
|
+
const LINE_WIDTH = 2;
|
|
35
|
+
|
|
36
|
+
/// One sample as the engine takes it. Time is unix milliseconds on the way in and seconds on the
|
|
37
|
+
/// way through: the engine counts in seconds, and a host that hands over milliseconds - which is
|
|
38
|
+
/// what `Date.now()` and `presentation.timeText` both deal in - should not have to know that.
|
|
39
|
+
function toSeries(points: readonly PnlPoint[]): AreaData[] {
|
|
40
|
+
const seen = new Set<number>();
|
|
41
|
+
const out: AreaData[] = [];
|
|
42
|
+
|
|
43
|
+
for (const point of [...points].sort((a, b) => a.time - b.time)) {
|
|
44
|
+
if (!Number.isFinite(point.time) || !Number.isFinite(point.value)) continue;
|
|
45
|
+
// The engine holds one value per moment. Two fills in the same second are two samples of
|
|
46
|
+
// one instant, and the later one is where the run actually stood.
|
|
47
|
+
const second = Math.floor(point.time / 1000);
|
|
48
|
+
if (seen.has(second)) out[out.length - 1] = { time: second as Time, value: point.value };
|
|
49
|
+
else out.push({ time: second as Time, value: point.value });
|
|
50
|
+
seen.add(second);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class EquityWidget {
|
|
56
|
+
static TYPE = ControlTypes.Equity;
|
|
57
|
+
|
|
58
|
+
rootEl: HTMLElement;
|
|
59
|
+
// `//` rather than `///` from here down — see the note in positions-widget.
|
|
60
|
+
_host: TradingHost;
|
|
61
|
+
_closeBtn: HTMLElement | null;
|
|
62
|
+
_resetBtn: HTMLElement | null;
|
|
63
|
+
_lastEl: HTMLElement | null;
|
|
64
|
+
_hoverEl: HTMLElement | null;
|
|
65
|
+
_emptyEl: HTMLElement | null;
|
|
66
|
+
_chartEl: HTMLElement | null;
|
|
67
|
+
_chart: IChartApi | null;
|
|
68
|
+
_series: ISeriesApi<AreaData> | null;
|
|
69
|
+
_points: PnlPoint[];
|
|
70
|
+
_resizeObserver: ResizeObserver | null;
|
|
71
|
+
|
|
72
|
+
static create(hostEl: HTMLElement, state: Record<string, unknown>, deps: EquityDeps): EquityWidget {
|
|
73
|
+
const host = assertHost(deps?.host, 'EquityWidget');
|
|
74
|
+
const root = EquityWidget._buildRoot(host);
|
|
75
|
+
root.id = makePanelId(EquityWidget.TYPE);
|
|
76
|
+
hostEl.appendChild(root);
|
|
77
|
+
return new EquityWidget(root, state || {}, deps);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static _buildRoot(host: TradingHost): HTMLElement {
|
|
81
|
+
const title = host.t('Equity');
|
|
82
|
+
return makePanelRoot('equity-panel', title, [
|
|
83
|
+
makeElement('div', 'panel-header', {}, [
|
|
84
|
+
makeElement('span', '', {}, [title]),
|
|
85
|
+
makeElement('span', 'equity-last', {}, []),
|
|
86
|
+
makeIconButton('bt-icon-btn equity-reset-btn', host.t('ResetView'), 'bi-arrow-clockwise', { type: 'button' }),
|
|
87
|
+
makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
|
|
88
|
+
]),
|
|
89
|
+
makeElement('div', 'panel-body', {}, [
|
|
90
|
+
makeElement('div', 'equity-chart', { role: 'img', 'aria-label': host.t('PnLChart') }, [
|
|
91
|
+
// Over the curve rather than in the panel header, for the same reason the
|
|
92
|
+
// smile's is: a reading about a moment belongs beside that moment, and a host
|
|
93
|
+
// that lifts panel headers into a tab strip would put it in a tab title.
|
|
94
|
+
makeElement('div', 'equity-hover', {}, []),
|
|
95
|
+
makeElement('div', 'equity-empty', {}, [host.t('NoEquity')]),
|
|
96
|
+
]),
|
|
97
|
+
]),
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
constructor(rootEl: HTMLElement, _state: Record<string, unknown>, deps: EquityDeps) {
|
|
102
|
+
this._host = assertHost(deps?.host, 'EquityWidget');
|
|
103
|
+
|
|
104
|
+
this.rootEl = rootEl;
|
|
105
|
+
this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
|
|
106
|
+
this._resetBtn = this.rootEl.querySelector('.equity-reset-btn');
|
|
107
|
+
this._lastEl = this.rootEl.querySelector('.equity-last');
|
|
108
|
+
this._hoverEl = this.rootEl.querySelector('.equity-hover');
|
|
109
|
+
this._emptyEl = this.rootEl.querySelector('.equity-empty');
|
|
110
|
+
this._chartEl = this.rootEl.querySelector('.equity-chart');
|
|
111
|
+
this._chart = null;
|
|
112
|
+
this._series = null;
|
|
113
|
+
this._points = [];
|
|
114
|
+
this._resizeObserver = null;
|
|
115
|
+
|
|
116
|
+
this._closeBtn?.addEventListener('click', (e) => { e.preventDefault(); this._host.close(); });
|
|
117
|
+
this._resetBtn?.addEventListener('click', (e) => { e.preventDefault(); this.resetZoom(); });
|
|
118
|
+
|
|
119
|
+
// The panel has no size until it is laid out, and a docked one is resized afterwards.
|
|
120
|
+
// The engine sizes its canvases only through resize(), so it is told rather than left to
|
|
121
|
+
// notice.
|
|
122
|
+
if (this._chartEl !== null && typeof ResizeObserver !== 'undefined') {
|
|
123
|
+
this._resizeObserver = new ResizeObserver(() => this._fit());
|
|
124
|
+
this._resizeObserver.observe(this._chartEl);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this._render();
|
|
128
|
+
this._host.register(this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
dispose(): void {
|
|
132
|
+
try { this._resizeObserver?.disconnect(); } catch { /* already torn down */ }
|
|
133
|
+
this._resizeObserver = null;
|
|
134
|
+
try { this._chart?.remove(); } catch { /* already gone */ }
|
|
135
|
+
this._chart = null;
|
|
136
|
+
this._series = null;
|
|
137
|
+
this._host.unregister(this);
|
|
138
|
+
try { this.rootEl.remove(); } catch { /* already detached */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Show this run. The whole curve at once: it is a cumulative figure, so a set of points that
|
|
142
|
+
/// no longer contains an earlier sample is a different run rather than a longer one.
|
|
143
|
+
///
|
|
144
|
+
/// `time` is unix milliseconds.
|
|
145
|
+
update(points: PnlPoint[]): void {
|
|
146
|
+
this._points = points || [];
|
|
147
|
+
this._render();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Show the whole run again, after a zoom.
|
|
151
|
+
resetZoom(): void {
|
|
152
|
+
try { this._chart?.timeScale().fitContent(); } catch { /* no chart yet */ }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The engine, for a host that wants to add to this chart - a benchmark line, a marker at a
|
|
156
|
+
/// drawdown. Null until there is something to draw.
|
|
157
|
+
chart(): IChartApi | null {
|
|
158
|
+
return this._chart;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Built on first use rather than in the constructor: the engine measures its container, and
|
|
162
|
+
// a panel that has not been laid out yet has none.
|
|
163
|
+
_ensureChart(): boolean {
|
|
164
|
+
if (this._chart !== null) return true;
|
|
165
|
+
if (this._chartEl === null) return false;
|
|
166
|
+
|
|
167
|
+
const palette = this._host.presentation.canvasPalette();
|
|
168
|
+
this._chart = createChart(this._chartEl, {
|
|
169
|
+
layout: {
|
|
170
|
+
background: { type: 'solid', color: 'transparent' },
|
|
171
|
+
textColor: palette.grid,
|
|
172
|
+
fontFamily: palette.font,
|
|
173
|
+
fontSize: 11,
|
|
174
|
+
attributionLogo: false,
|
|
175
|
+
},
|
|
176
|
+
grid: { vertLines: { color: palette.grid }, horzLines: { color: palette.grid } },
|
|
177
|
+
crosshair: { mode: CrosshairMode.Normal },
|
|
178
|
+
rightPriceScale: { borderColor: palette.grid },
|
|
179
|
+
timeScale: {
|
|
180
|
+
borderColor: palette.grid,
|
|
181
|
+
timeVisible: true,
|
|
182
|
+
secondsVisible: true,
|
|
183
|
+
// The engine dates an axis in UTC unless told otherwise, and everything else on
|
|
184
|
+
// the page - the readout beside this one included - reads in the browser's zone.
|
|
185
|
+
// Two labels for one instant, three hours apart, is worse than either.
|
|
186
|
+
timeZone: browserTimeZone(),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this._series = this._chart.addSeries(AreaSeries, {
|
|
191
|
+
lineWidth: LINE_WIDTH,
|
|
192
|
+
lineColor: palette.up,
|
|
193
|
+
topColor: withAlpha(palette.up, FILL_TOP_ALPHA),
|
|
194
|
+
bottomColor: withAlpha(palette.up, FILL_BOTTOM_ALPHA),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// The readout. The engine says which point the pointer is over; wording it is the host's,
|
|
198
|
+
// which is why this is here rather than in the engine's own crosshair label.
|
|
199
|
+
this._chart.subscribeCrosshairMove((param) => this._renderHover(param));
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_render(): void {
|
|
204
|
+
const series = toSeries(this._points);
|
|
205
|
+
const empty = series.length < 2;
|
|
206
|
+
|
|
207
|
+
if (this._emptyEl !== null) this._emptyEl.hidden = !empty;
|
|
208
|
+
this._renderLast(empty ? null : series[series.length - 1].value);
|
|
209
|
+
if (empty) return;
|
|
210
|
+
if (!this._ensureChart() || this._series === null) return;
|
|
211
|
+
|
|
212
|
+
// Coloured by where the run ended, the way the sparkline is: a run that peaked and gave
|
|
213
|
+
// it all back is a loss, and the colour says so.
|
|
214
|
+
const palette = this._host.presentation.canvasPalette();
|
|
215
|
+
const last = series[series.length - 1].value;
|
|
216
|
+
const colour = last >= 0 ? palette.up : palette.down;
|
|
217
|
+
this._series.applyOptions({
|
|
218
|
+
lineColor: colour,
|
|
219
|
+
topColor: withAlpha(colour, FILL_TOP_ALPHA),
|
|
220
|
+
bottomColor: withAlpha(colour, FILL_BOTTOM_ALPHA),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
this._series.setData(series);
|
|
224
|
+
this._fit();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_fit(): void {
|
|
228
|
+
if (this._chart === null || this._chartEl === null) return;
|
|
229
|
+
const rect = this._chartEl.getBoundingClientRect();
|
|
230
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
231
|
+
try {
|
|
232
|
+
this._chart.resize(rect.width, rect.height);
|
|
233
|
+
} catch { /* the engine is between frames */ }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Where the run stands, in the header.
|
|
237
|
+
_renderLast(value: number | null): void {
|
|
238
|
+
const el = this._lastEl;
|
|
239
|
+
if (el === null) return;
|
|
240
|
+
|
|
241
|
+
el.textContent = value === null ? '' : formatPnl(value);
|
|
242
|
+
el.className = value === null
|
|
243
|
+
? 'equity-last'
|
|
244
|
+
: `equity-last ${this._host.presentation.pnlClass(value)}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// What the pointer is over. The engine hands over the moment and the value at it; the moment
|
|
248
|
+
// is worded by the host, because what a time reads as is the page's business and not this
|
|
249
|
+
// package's.
|
|
250
|
+
_renderHover(param: CrosshairEvent): void {
|
|
251
|
+
const el = this._hoverEl;
|
|
252
|
+
if (el === null) return;
|
|
253
|
+
|
|
254
|
+
const point = this._series === null ? undefined : param.seriesData.get(this._series);
|
|
255
|
+
const value = (point as { value?: number } | undefined)?.value;
|
|
256
|
+
|
|
257
|
+
// Off the series entirely: the engine reports a crosshair with no time when the pointer
|
|
258
|
+
// has left the plot, and a stale readout is worse than none.
|
|
259
|
+
if (param.time === null || value === undefined || !Number.isFinite(value)) {
|
|
260
|
+
el.textContent = '';
|
|
261
|
+
el.className = 'equity-hover';
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Back to milliseconds for the host: the engine counts in seconds, and every other moment
|
|
266
|
+
// this package hands over is a Date.
|
|
267
|
+
const moment = new Date(Number(param.time) * 1000);
|
|
268
|
+
el.textContent = `${this._host.presentation.timeText(moment)} ${formatPnl(value)}`;
|
|
269
|
+
el.className = `equity-hover ${this._host.presentation.pnlClass(value)}`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// The zone a moment reads in: the browser's own, which is the zone
|
|
274
|
+
/// `presentation.timeText` words one in. UTC when the environment cannot say.
|
|
275
|
+
function browserTimeZone(): string {
|
|
276
|
+
try {
|
|
277
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
278
|
+
} catch {
|
|
279
|
+
return 'UTC';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// A colour at a given opacity.
|
|
284
|
+
///
|
|
285
|
+
/// The host answers with whatever CSS understands, so this cannot take it apart - it wraps it
|
|
286
|
+
/// instead, which `color-mix` accepts for every form a token can be written in.
|
|
287
|
+
function withAlpha(colour: string, alpha: number): string {
|
|
288
|
+
return `color-mix(in srgb, ${colour} ${Math.round(alpha * 100)}%, transparent)`;
|
|
289
|
+
}
|