@stocksharp/trading-controls 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +92 -6
  2. package/dist/esm/active-orders-widget.js +9 -0
  3. package/dist/esm/active-orders-widget.js.map +1 -1
  4. package/dist/esm/control-types.js +3 -0
  5. package/dist/esm/control-types.js.map +1 -1
  6. package/dist/esm/grid-menu.js +54 -0
  7. package/dist/esm/grid-menu.js.map +1 -0
  8. package/dist/esm/index.js +8 -0
  9. package/dist/esm/index.js.map +1 -1
  10. package/dist/esm/order-entry-widget.js +654 -0
  11. package/dist/esm/order-entry-widget.js.map +1 -0
  12. package/dist/esm/orderbook-depth.js +51 -0
  13. package/dist/esm/orderbook-depth.js.map +1 -0
  14. package/dist/esm/orderbook-widget.js +792 -0
  15. package/dist/esm/orderbook-widget.js.map +1 -0
  16. package/dist/esm/positions-widget.js +9 -0
  17. package/dist/esm/positions-widget.js.map +1 -1
  18. package/dist/esm/trade-history-widget.js +8 -0
  19. package/dist/esm/trade-history-widget.js.map +1 -1
  20. package/dist/esm/tradefeed-aggregator.js +58 -0
  21. package/dist/esm/tradefeed-aggregator.js.map +1 -0
  22. package/dist/esm/tradefeed-bubbles.js +131 -0
  23. package/dist/esm/tradefeed-bubbles.js.map +1 -0
  24. package/dist/esm/tradefeed-widget.js +692 -0
  25. package/dist/esm/tradefeed-widget.js.map +1 -0
  26. package/dist/esm/trading-host.js +2 -0
  27. package/dist/esm/trading-host.js.map +1 -1
  28. package/dist/esm/watchlist-widget.js +2 -0
  29. package/dist/esm/watchlist-widget.js.map +1 -1
  30. package/dist/sstradingcontrols.js +3346 -25
  31. package/dist/sstradingcontrols.js.map +3 -3
  32. package/dist/types/active-orders-widget.d.ts.map +1 -1
  33. package/dist/types/control-types.d.ts +3 -0
  34. package/dist/types/control-types.d.ts.map +1 -1
  35. package/dist/types/grid-menu.d.ts +4 -0
  36. package/dist/types/grid-menu.d.ts.map +1 -0
  37. package/dist/types/index.d.ts +13 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/order-entry-widget.d.ts +86 -0
  40. package/dist/types/order-entry-widget.d.ts.map +1 -0
  41. package/dist/types/orderbook-depth.d.ts +19 -0
  42. package/dist/types/orderbook-depth.d.ts.map +1 -0
  43. package/dist/types/orderbook-widget.d.ts +105 -0
  44. package/dist/types/orderbook-widget.d.ts.map +1 -0
  45. package/dist/types/positions-widget.d.ts.map +1 -1
  46. package/dist/types/trade-history-widget.d.ts.map +1 -1
  47. package/dist/types/tradefeed-aggregator.d.ts +15 -0
  48. package/dist/types/tradefeed-aggregator.d.ts.map +1 -0
  49. package/dist/types/tradefeed-bubbles.d.ts +39 -0
  50. package/dist/types/tradefeed-bubbles.d.ts.map +1 -0
  51. package/dist/types/tradefeed-widget.d.ts +78 -0
  52. package/dist/types/tradefeed-widget.d.ts.map +1 -0
  53. package/dist/types/trading-data.d.ts +22 -0
  54. package/dist/types/trading-data.d.ts.map +1 -1
  55. package/dist/types/trading-host.d.ts +8 -0
  56. package/dist/types/trading-host.d.ts.map +1 -1
  57. package/dist/types/watchlist-widget.d.ts.map +1 -1
  58. package/package.json +21 -2
  59. package/screenshots/panels.jpg +0 -0
  60. package/src/active-orders-widget.ts +9 -0
  61. package/src/control-types.ts +3 -0
  62. package/src/grid-menu.ts +67 -0
  63. package/src/index.ts +31 -0
  64. package/src/order-entry-widget.ts +733 -0
  65. package/src/orderbook-depth.ts +99 -0
  66. package/src/orderbook-widget.ts +896 -0
  67. package/src/positions-widget.ts +9 -0
  68. package/src/trade-history-widget.ts +8 -0
  69. package/src/tradefeed-aggregator.ts +95 -0
  70. package/src/tradefeed-bubbles.ts +194 -0
  71. package/src/tradefeed-widget.ts +744 -0
  72. package/src/trading-data.ts +168 -121
  73. package/src/trading-host.ts +326 -300
  74. package/src/watchlist-widget.ts +2 -0
  75. package/styles/theme.css +23 -0
  76. package/styles/trading-controls.css +911 -0
  77. package/translation-keys.json +103 -2
@@ -15,6 +15,7 @@
15
15
  import { formatPnl, formatPrice } from './formatters.js';
16
16
  import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
17
17
  import { ControlTypes } from './control-types.js';
18
+ import { makeGridMenu } from './grid-menu.js';
18
19
  import { TradingHost, assertHost } from './trading-host.js';
19
20
  import type { BalanceRow, PositionRow } from './trading-data.js';
20
21
  import { DataGrid, GridColumn, GridPinnedRow, GridPinnedPlacements } from '@stocksharp/grids/source/data-grid';
@@ -135,6 +136,11 @@ export class PositionsWidget {
135
136
  // Re-read on every render, so the cash figures track the live balance
136
137
  // without the widget having to repaint the position rows itself.
137
138
  pinnedRows: () => this._pinnedRows(),
139
+ contextMenu: makeGridMenu(this._host),
140
+ // Press to mark, drag or Shift to range, Ctrl to toggle, Ctrl+C
141
+ // to copy — the pinned cash row stays outside the selection the
142
+ // same way it stays outside the sort.
143
+ selection: 'multi',
138
144
  })
139
145
  : null;
140
146
 
@@ -145,6 +151,9 @@ export class PositionsWidget {
145
151
  }
146
152
 
147
153
  dispose(): void {
154
+ // The grid holds document-level listeners (the copy shortcut) that
155
+ // outlive a removed subtree — it has to be told, not just detached.
156
+ this._grid?.destroy();
148
157
  this._host.unregister(this);
149
158
  try { this.rootEl.remove(); } catch { /* already detached */ }
150
159
  }
@@ -15,6 +15,7 @@
15
15
  import { formatPrice, formatQty, formatTime } from './formatters.js';
16
16
  import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
17
17
  import { ControlTypes } from './control-types.js';
18
+ import { makeGridMenu } from './grid-menu.js';
18
19
  import { TradingHost, assertHost } from './trading-host.js';
19
20
  import type { TradeRow } from './trading-data.js';
20
21
  import { DataGrid, GridColumn } from '@stocksharp/grids/source/data-grid';
@@ -109,6 +110,10 @@ export class TradeHistoryWidget {
109
110
  defaultSort: { col: 'time', dir: 'desc' },
110
111
  rowKey: (t) => String(t.id),
111
112
  emptyText: this._host.t('No trade history'),
113
+ contextMenu: makeGridMenu(this._host),
114
+ // Press to mark, drag or Shift to range, Ctrl to toggle, Ctrl+C
115
+ // to copy — all the grid's own.
116
+ selection: 'multi',
112
117
  })
113
118
  : null;
114
119
 
@@ -116,6 +121,9 @@ export class TradeHistoryWidget {
116
121
  }
117
122
 
118
123
  dispose(): void {
124
+ // The grid holds document-level listeners (the copy shortcut) that
125
+ // outlive a removed subtree — it has to be told, not just detached.
126
+ this._grid?.destroy();
119
127
  this._host.unregister(this);
120
128
  try { this.rootEl.remove(); } catch { /* already detached */ }
121
129
  }
@@ -0,0 +1,95 @@
1
+ // Bubble compaction for a tape.
2
+ //
3
+ // One lane of a bubble chart is one symbol's ticks, and a busy symbol delivers
4
+ // far more of them than a lane has room for: drawn one circle per print, the
5
+ // lane melts into a solid wall. Bucketing by time slice and side turns that
6
+ // back into something readable — at most two bubbles per slice, each placed at
7
+ // its slice's volume-weighted average price and sized by its summed volume.
8
+ //
9
+ // Pure: no DOM, no host, no clock. The trade feed renders what comes out of
10
+ // here, which is what lets the arithmetic a busy symbol depends on be covered
11
+ // directly rather than through a canvas.
12
+ import type { OrderSide } from './trading-data.js';
13
+
14
+ /// One print, with the two things the tape has already decided about it: which
15
+ /// direction it went, and where it sits on the shared time axis.
16
+ ///
17
+ /// `buy` is resolved by the host's presentation before a tick reaches this
18
+ /// module — the wire spells a side three ways and normalising it is the host's
19
+ /// job, not this file's — while `side` keeps the raw value, because wording it
20
+ /// is the host's job too and only it can do that. `index` is the tick's
21
+ /// position in the WHOLE feed rather than in its lane, so two symbols with
22
+ /// wildly different prices still flow along one time axis.
23
+ export interface FeedTick {
24
+ symbol: string;
25
+ side: OrderSide;
26
+ buy: boolean;
27
+ price: number;
28
+ quantity: number;
29
+ time: string;
30
+ index: number;
31
+ }
32
+
33
+ /// One bubble: a tick, or the slice of ticks it stands for.
34
+ export interface FeedBubble extends FeedTick {
35
+ /// How many ticks this bubble represents — 1 when it is a tick itself, and
36
+ /// what the tooltip reads to say "VWAP" instead of "Price".
37
+ count: number;
38
+ }
39
+
40
+ /// Bucket one lane's ticks into at most `maxBucketsPerSide` bubbles per side.
41
+ ///
42
+ /// Below that budget every tick passes through as its own one-tick bubble, so a
43
+ /// sparse symbol still shows every print. Above it, each (side, slice) merges
44
+ /// into one bubble. `ticks` is newest-first, which is how the tape holds them,
45
+ /// so the first slice is the most recent block.
46
+ export function aggregateBubbles(ticks: FeedTick[], maxBucketsPerSide: number): FeedBubble[] {
47
+ if (!ticks || ticks.length === 0) return [];
48
+ const budget = Math.max(1, Math.floor(maxBucketsPerSide));
49
+
50
+ // The two directions compete for the same horizontal room, so each gets its
51
+ // own budget rather than the busier one crowding the other out.
52
+ const buys: FeedTick[] = [];
53
+ const sells: FeedTick[] = [];
54
+ for (const tick of ticks) (tick.buy ? buys : sells).push(tick);
55
+
56
+ const bubbles: FeedBubble[] = [];
57
+ for (const side of [buys, sells]) {
58
+ if (side.length === 0) continue;
59
+ if (side.length <= budget) {
60
+ for (const tick of side) bubbles.push({ ...tick, count: 1 });
61
+ continue;
62
+ }
63
+ const bucketSize = Math.ceil(side.length / budget);
64
+ for (let start = 0; start < side.length; start += bucketSize)
65
+ bubbles.push(merge(side.slice(start, start + bucketSize)));
66
+ }
67
+ return bubbles;
68
+ }
69
+
70
+ /// One slice as a single bubble: total volume, volume-weighted price, and the
71
+ /// slice's middle tick for its position on the time axis.
72
+ function merge(slice: FeedTick[]): FeedBubble {
73
+ let quantity = 0;
74
+ let weighted = 0;
75
+ for (const tick of slice) {
76
+ const q = tick.quantity || 0;
77
+ quantity += q;
78
+ weighted += q * tick.price;
79
+ }
80
+ // A slice can legitimately total zero volume (an all-zero-quantity tape is
81
+ // rare live and ordinary in a fixture), and dividing by it would place the
82
+ // bubble at NaN — nowhere, silently. Its first price is the honest answer.
83
+ const price = quantity > 0 ? weighted / quantity : slice[0].price;
84
+ const middle = slice[Math.floor(slice.length / 2)];
85
+ return {
86
+ symbol: slice[0].symbol,
87
+ side: slice[0].side,
88
+ buy: slice[0].buy,
89
+ price,
90
+ quantity,
91
+ time: middle.time,
92
+ index: middle.index,
93
+ count: slice.length,
94
+ };
95
+ }
@@ -0,0 +1,194 @@
1
+ // Where every bubble, rule and label of a bubble chart goes.
2
+ //
3
+ // The trade feed's second rendering is a scatter of prints: time along X,
4
+ // price up Y, volume as the radius, direction as the colour. It is drawn on a
5
+ // canvas, which is the one surface a class name cannot reach — so the geometry
6
+ // is computed here, as numbers, and the control does nothing but stroke and
7
+ // fill what comes back.
8
+ //
9
+ // Pure, and that is the point: a fake canvas that swallows drawing calls proves
10
+ // nothing, while the arithmetic below — lanes sharing one time axis, a price
11
+ // scale per symbol, a radius that stays inside its lane — is exactly what can
12
+ // be wrong. It is covered directly.
13
+ import { aggregateBubbles, type FeedBubble, type FeedTick } from './tradefeed-aggregator.js';
14
+
15
+ /// One symbol's prints. A feed following a single symbol has one unnamed lane;
16
+ /// pinning extras splits the height into one lane each so a $76k symbol and a
17
+ /// $270 one both stay visible instead of one flattening the other.
18
+ export interface BubbleLane {
19
+ symbol: string;
20
+ ticks: FeedTick[];
21
+ }
22
+
23
+ export interface BubbleLayoutInput {
24
+ width: number;
25
+ height: number;
26
+ lanes: BubbleLane[];
27
+ /// How many ticks the whole feed holds. The X axis is a tick's index in
28
+ /// that list, so lanes flow along one shared time axis rather than each
29
+ /// stretching its own count across the full width.
30
+ total: number;
31
+ }
32
+
33
+ /// One bubble, placed.
34
+ export interface BubbleShape {
35
+ x: number;
36
+ y: number;
37
+ radius: number;
38
+ bubble: FeedBubble;
39
+ }
40
+
41
+ /// One price label on the right-hand axis, and the rule leading to it.
42
+ export interface BubbleAxisTick {
43
+ text: string;
44
+ y: number;
45
+ }
46
+
47
+ /// One lane, placed. `label` is null for a feed following a single symbol —
48
+ /// naming the only lane says nothing — and `separatorY` is null for the first
49
+ /// lane, which has nothing above it to be divided from.
50
+ export interface BubbleLaneShape {
51
+ symbol: string;
52
+ label: { x: number; y: number } | null;
53
+ separatorY: number | null;
54
+ ticks: BubbleAxisTick[];
55
+ }
56
+
57
+ export interface BubbleLayout {
58
+ /// Left edge of the axis strip: rules stop just short of it, labels start
59
+ /// just past it.
60
+ axisX: number;
61
+ /// Where an axis label's text begins.
62
+ labelX: number;
63
+ lanes: BubbleLaneShape[];
64
+ /// Every bubble in draw order — oldest first, so the newest print lands on
65
+ /// top of what it overlaps. Hit-testing walks this backwards for the same
66
+ /// reason.
67
+ bubbles: BubbleShape[];
68
+ }
69
+
70
+ // Geometry. These are the chart's proportions, not the host's looks: a canvas
71
+ // has no cascade to read them from, and moving them into CSS would only mean
72
+ // reading them back out again.
73
+ const PAD_X = 10;
74
+ const PAD_Y = 10;
75
+ const AXIS_WIDTH = 56;
76
+ const LABEL_GAP = 4;
77
+ const LANE_GAP = 2;
78
+ const MIN_LANE_HEIGHT = 20;
79
+ const AXIS_TICKS = 3;
80
+ const MIN_RADIUS = 2;
81
+ const MAX_RADIUS = 22;
82
+ /// A bubble may grow to this share of its lane's height, so a lane with one
83
+ /// enormous print does not draw a circle taller than the lane it sits in.
84
+ const RADIUS_LANE_SHARE = 0.4;
85
+ /// Horizontal room one bucket wants. Narrower and the lane reads as a wall;
86
+ /// wider and a sparse symbol turns into a handful of lonely dots.
87
+ const BUCKET_WIDTH = 8;
88
+ const MIN_BUCKETS = 6;
89
+ const MAX_BUCKETS = 30;
90
+
91
+ /// Place every lane and every bubble for one paint.
92
+ export function layoutBubbles(input: BubbleLayoutInput): BubbleLayout {
93
+ const { width, height, total } = input;
94
+ const axisX = Math.max(PAD_X + 1, width - AXIS_WIDTH);
95
+ const drawWidth = Math.max(1, axisX - PAD_X);
96
+ const layout: BubbleLayout = { axisX, labelX: axisX + LABEL_GAP, lanes: [], bubbles: [] };
97
+
98
+ const lanes = input.lanes.filter(lane => lane.ticks.length > 0);
99
+ if (lanes.length === 0) return layout;
100
+
101
+ const named = input.lanes.length > 1;
102
+ const laneHeight = Math.max(
103
+ MIN_LANE_HEIGHT,
104
+ (height - 2 * PAD_Y - LANE_GAP * (input.lanes.length - 1)) / input.lanes.length);
105
+
106
+ // Placed against the DECLARED lanes, not the non-empty ones: a symbol that
107
+ // has not printed yet keeps its slot rather than letting the lanes below it
108
+ // slide up and back down on its first tick.
109
+ input.lanes.forEach((lane, position) => {
110
+ const laneTop = named ? PAD_Y + position * (laneHeight + LANE_GAP) : PAD_Y;
111
+ const laneSpan = named ? laneHeight : Math.max(MIN_LANE_HEIGHT, height - 2 * PAD_Y);
112
+ const scale = priceScale(lane.ticks, laneTop, laneSpan);
113
+
114
+ layout.lanes.push({
115
+ symbol: lane.symbol,
116
+ label: named ? { x: LABEL_GAP, y: laneTop + 2 } : null,
117
+ separatorY: named && position > 0 ? laneTop - LANE_GAP / 2 : null,
118
+ ticks: lane.ticks.length === 0 ? [] : axisTicks(scale),
119
+ });
120
+
121
+ if (lane.ticks.length === 0) return;
122
+ const buckets = Math.max(MIN_BUCKETS, Math.min(MAX_BUCKETS, Math.floor(drawWidth / BUCKET_WIDTH)));
123
+ const bubbles = aggregateBubbles(lane.ticks, buckets);
124
+ let maxQuantity = 0;
125
+ for (const bubble of bubbles) if (bubble.quantity > maxQuantity) maxQuantity = bubble.quantity;
126
+ const quantityScale = Math.max(maxQuantity, Number.EPSILON);
127
+ const radiusCap = Math.min(MAX_RADIUS, laneSpan * RADIUS_LANE_SHARE);
128
+
129
+ for (const bubble of bubbles) {
130
+ layout.bubbles.push({
131
+ // Newest print (index 0) at the right edge, oldest at the left.
132
+ x: PAD_X + drawWidth * (total <= 1 ? 1 : 1 - bubble.index / (total - 1)),
133
+ y: scale.top + scale.height * (1 - (bubble.price - scale.min) / scale.range),
134
+ radius: MIN_RADIUS + Math.sqrt(Math.max(0, bubble.quantity) / quantityScale) * radiusCap,
135
+ bubble,
136
+ });
137
+ }
138
+ });
139
+
140
+ // Oldest first: a later print draws over an earlier one, and the hit test
141
+ // reads the same list backwards so the circle on top is the one picked.
142
+ layout.bubbles.sort((left, right) => right.bubble.index - left.bubble.index);
143
+ return layout;
144
+ }
145
+
146
+ interface PriceScale {
147
+ top: number;
148
+ height: number;
149
+ min: number;
150
+ range: number;
151
+ }
152
+
153
+ /// The lane's price range, as the Y axis. `range` is never zero — a lane whose
154
+ /// prints are all at one price would otherwise divide by it — so a flat lane
155
+ /// draws along its own top edge rather than nowhere.
156
+ function priceScale(ticks: FeedTick[], top: number, height: number): PriceScale {
157
+ let min = Infinity;
158
+ let max = -Infinity;
159
+ for (const tick of ticks) {
160
+ if (tick.price < min) min = tick.price;
161
+ if (tick.price > max) max = tick.price;
162
+ }
163
+ if (!isFinite(min) || !isFinite(max)) { min = 0; max = 0; }
164
+ return { top, height, min, range: Math.max(max - min, Number.EPSILON) };
165
+ }
166
+
167
+ /// Three labels down the lane: its high, its middle and its low.
168
+ function axisTicks(scale: PriceScale): BubbleAxisTick[] {
169
+ const format = priceFormatter(scale.min, scale.min + scale.range);
170
+ const ticks: BubbleAxisTick[] = [];
171
+ for (let step = 0; step < AXIS_TICKS; step++) {
172
+ const fraction = step / (AXIS_TICKS - 1);
173
+ ticks.push({
174
+ text: format((scale.min + scale.range) - scale.range * fraction),
175
+ y: scale.top + scale.height * fraction,
176
+ });
177
+ }
178
+ return ticks;
179
+ }
180
+
181
+ /// Decimals chosen from the lane's magnitude. A fixed precision is wrong at
182
+ /// both ends: two decimals on a $76,000 print is noise, and none on a $0.04 one
183
+ /// collapses the whole lane onto a single label.
184
+ export function priceFormatter(min: number, max: number): (price: number) => string {
185
+ const magnitude = Math.max(Math.abs(min), Math.abs(max));
186
+ let digits = 6;
187
+ if (magnitude >= 1000) digits = 0;
188
+ else if (magnitude >= 10) digits = 2;
189
+ else if (magnitude >= 0.1) digits = 4;
190
+ return (price: number) => price.toLocaleString(undefined, {
191
+ minimumFractionDigits: digits,
192
+ maximumFractionDigits: digits,
193
+ });
194
+ }