@stocksharp/trading-controls 0.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.
- package/LICENSE +30 -0
- package/NOTICE +21 -0
- package/README.md +240 -0
- package/dist/esm/active-orders-widget.js +382 -0
- package/dist/esm/active-orders-widget.js.map +1 -0
- package/dist/esm/control-types.js +23 -0
- package/dist/esm/control-types.js.map +1 -0
- package/dist/esm/dom.js +55 -0
- package/dist/esm/dom.js.map +1 -0
- package/dist/esm/formatters.js +69 -0
- package/dist/esm/formatters.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/positions-widget.js +258 -0
- package/dist/esm/positions-widget.js.map +1 -0
- package/dist/esm/trade-history-widget.js +197 -0
- package/dist/esm/trade-history-widget.js.map +1 -0
- package/dist/esm/trading-data.js +26 -0
- package/dist/esm/trading-data.js.map +1 -0
- package/dist/esm/trading-host.js +79 -0
- package/dist/esm/trading-host.js.map +1 -0
- package/dist/esm/watchlist-widget.js +481 -0
- package/dist/esm/watchlist-widget.js.map +1 -0
- package/dist/sstradingcontrols.js +1740 -0
- package/dist/sstradingcontrols.js.map +7 -0
- package/dist/types/active-orders-widget.d.ts +52 -0
- package/dist/types/active-orders-widget.d.ts.map +1 -0
- package/dist/types/control-types.d.ts +8 -0
- package/dist/types/control-types.d.ts.map +1 -0
- package/dist/types/dom.d.ts +6 -0
- package/dist/types/dom.d.ts.map +1 -0
- package/dist/types/formatters.d.ts +8 -0
- package/dist/types/formatters.d.ts.map +1 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/positions-widget.d.ts +37 -0
- package/dist/types/positions-widget.d.ts.map +1 -0
- package/dist/types/trade-history-widget.d.ts +25 -0
- package/dist/types/trade-history-widget.d.ts.map +1 -0
- package/dist/types/trading-data.d.ts +55 -0
- package/dist/types/trading-data.d.ts.map +1 -0
- package/dist/types/trading-host.d.ts +62 -0
- package/dist/types/trading-host.d.ts.map +1 -0
- package/dist/types/watchlist-widget.d.ts +60 -0
- package/dist/types/watchlist-widget.d.ts.map +1 -0
- package/package.json +129 -0
- package/src/active-orders-widget.ts +411 -0
- package/src/control-types.ts +24 -0
- package/src/dom.ts +68 -0
- package/src/formatters.ts +72 -0
- package/src/index.ts +52 -0
- package/src/positions-widget.ts +305 -0
- package/src/trade-history-widget.ts +221 -0
- package/src/trading-data.ts +121 -0
- package/src/trading-host.ts +300 -0
- package/src/watchlist-widget.ts +513 -0
- package/styles/theme.css +86 -0
- package/styles/trading-controls.css +469 -0
- package/translation-keys.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
StockSharp Custom License Notice
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2010-present StockSharp Platform LLC and/or its affiliates.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
All source code, binaries, documentation, examples, media assets,
|
|
7
|
+
configuration files, and other materials contained in this repository are
|
|
8
|
+
the proprietary property of StockSharp, unless a file or third-party notice
|
|
9
|
+
explicitly states otherwise.
|
|
10
|
+
|
|
11
|
+
This repository is not licensed under the Apache License, the MIT License,
|
|
12
|
+
or any other general-purpose open source license.
|
|
13
|
+
Viewing, downloading, copying, building, modifying, using, distributing, or
|
|
14
|
+
otherwise accessing any part of this repository is permitted only under the
|
|
15
|
+
StockSharp End User License Agreement and other applicable StockSharp terms
|
|
16
|
+
published on the official StockSharp website:
|
|
17
|
+
|
|
18
|
+
https://stocksharp.com/en/products/eula/
|
|
19
|
+
|
|
20
|
+
StockSharp may update its license terms on the official website. Users are
|
|
21
|
+
responsible for monitoring the official StockSharp website and complying
|
|
22
|
+
with the then-current terms. If this notice conflicts with the EULA or other
|
|
23
|
+
terms published on the official StockSharp website, the website terms control.
|
|
24
|
+
|
|
25
|
+
Nothing in this repository grants any rights to StockSharp trademarks,
|
|
26
|
+
service marks, product names, logos, or other brand assets except as
|
|
27
|
+
expressly permitted by StockSharp in writing.
|
|
28
|
+
|
|
29
|
+
Third-party components, if any, remain subject to their respective license
|
|
30
|
+
terms and notices.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
StockSharp Notice
|
|
2
|
+
|
|
3
|
+
StockSharp Platform LLC
|
|
4
|
+
https://stocksharp.com/
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2010-present StockSharp Platform LLC and/or its affiliates.
|
|
7
|
+
All rights reserved.
|
|
8
|
+
|
|
9
|
+
The materials in this repository are proprietary to StockSharp unless a file
|
|
10
|
+
or third-party notice explicitly states otherwise. Use of these materials is
|
|
11
|
+
governed by the StockSharp End User License Agreement and other applicable
|
|
12
|
+
terms published on the official StockSharp website:
|
|
13
|
+
|
|
14
|
+
https://stocksharp.com/en/products/eula/
|
|
15
|
+
|
|
16
|
+
StockSharp may update its license terms on the official website. Users are
|
|
17
|
+
responsible for monitoring the official StockSharp website and complying
|
|
18
|
+
with the then-current terms.
|
|
19
|
+
|
|
20
|
+
This repository may include third-party components. Those components remain
|
|
21
|
+
subject to their respective license terms and notices.
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# StockSharp JS Trading Controls
|
|
2
|
+
|
|
3
|
+
[](https://github.com/StockSharp/JS-TradingControls/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@stocksharp/trading-controls)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**StockSharp JS Trading Controls** are the browser panels a trading screen is
|
|
8
|
+
made of: an **active orders** blotter with inline edit, a **positions** blotter
|
|
9
|
+
with a pinned cash balance, a **trade history** blotter, and a **watchlist**
|
|
10
|
+
with live quotes, favourites and category tabs.
|
|
11
|
+
|
|
12
|
+
Each control builds its own DOM, renders its own table through
|
|
13
|
+
[`@stocksharp/grids`](https://www.npmjs.com/package/@stocksharp/grids), and reaches
|
|
14
|
+
the outside world through exactly one object — a `TradingHost`.
|
|
15
|
+
|
|
16
|
+
[StockSharp website](https://stocksharp.com/) ·
|
|
17
|
+
[GitHub repository](https://github.com/StockSharp/JS-TradingControls) ·
|
|
18
|
+
[Issue tracker](https://github.com/StockSharp/JS-TradingControls/issues)
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @stocksharp/trading-controls
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { PositionsWidget } from '@stocksharp/trading-controls';
|
|
28
|
+
import '@stocksharp/trading-controls/styles.css';
|
|
29
|
+
// Optional: a working dark/light palette, if your page has none of its own.
|
|
30
|
+
import '@stocksharp/trading-controls/theme.css';
|
|
31
|
+
|
|
32
|
+
const panel = PositionsWidget.create(document.querySelector('#positions')!, {}, {
|
|
33
|
+
host, // see "The host port" below
|
|
34
|
+
closePosition: (pf, instrument, symbol) => api.close(pf, instrument),
|
|
35
|
+
reversePosition: (pf, instrument, symbol) => api.reverse(pf, instrument),
|
|
36
|
+
refreshPositions: () => reload(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
panel.update(positions);
|
|
40
|
+
panel.updateBalance({ available: 1000, locked: 250, total: 1250 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The package also ships a ready-to-use browser bundle exposed as
|
|
44
|
+
`window.SSTradingControls`:
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/@stocksharp/trading-controls@0.1.0/dist/sstradingcontrols.js"></script>
|
|
48
|
+
<script>
|
|
49
|
+
const { PositionsWidget } = window.SSTradingControls;
|
|
50
|
+
</script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## The host port is the whole API surface
|
|
54
|
+
|
|
55
|
+
A control imports no translator, no settings singleton, no panel registry and no
|
|
56
|
+
docking manager, and it never reads a `window` global. Everything it needs
|
|
57
|
+
arrives as one `TradingHost`:
|
|
58
|
+
|
|
59
|
+
| member | what the host answers |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `isPrimary` | is this the instance that speaks for the page? |
|
|
62
|
+
| `t(key, …args)` | translate; see [Text](#everything-a-control-shows-is-the-hosts) for what a key looks like |
|
|
63
|
+
| `presentation` | word and colour a side, an order type, a status, a P&L sign |
|
|
64
|
+
| `preferences` / `cache` | two stores — settings that must survive, and scratch |
|
|
65
|
+
| `trading` | the API, the market-data client, the active portfolio, an instrument picker |
|
|
66
|
+
| `ticker` | where a control reports the symbols it is showing |
|
|
67
|
+
| `allow(action)` | may the user do this? |
|
|
68
|
+
| `log(message)` | where diagnostics go |
|
|
69
|
+
| `close()` `spawn(state)` `persistState(patch)` `saveLayout()` | lifecycle calls back |
|
|
70
|
+
| `register` `unregister` `broadcast<T>` | the host's handle on live instances |
|
|
71
|
+
|
|
72
|
+
**Every member is required, and `assertHost` proves it before the control
|
|
73
|
+
renders anything.** A half-supplied host is how a control half-works: it draws,
|
|
74
|
+
it looks alive, and the one capability nobody wired is discovered by a user
|
|
75
|
+
clicking something that does nothing. So a missing member throws at construction
|
|
76
|
+
naming the path — `WatchlistWidget: host.trading.api.getExecutions is required` —
|
|
77
|
+
nested members included.
|
|
78
|
+
|
|
79
|
+
**Seven of them no shipped control calls**: `spawn`, `persistState`,
|
|
80
|
+
`saveLayout`, `log`, `trading.pickInstrument`, `marketData.resubscribe` and
|
|
81
|
+
`marketData.getOrders`. They are required anyway, and not on speculation — the
|
|
82
|
+
terminal this was extracted from has seven controls, and the three still on its
|
|
83
|
+
side of the boundary (order book, order entry, trade feed) are already written
|
|
84
|
+
against this same interface and call every one of those members. Narrowing the
|
|
85
|
+
port now and widening it again as each moves in would break every host twice.
|
|
86
|
+
If you are adopting only the four controls here, stubs are a correct answer: a
|
|
87
|
+
no-op `spawn`, a `log` that forwards to the console.
|
|
88
|
+
|
|
89
|
+
Two pairs are deliberately separate rather than merged:
|
|
90
|
+
|
|
91
|
+
- **`preferences` and `cache` are two stores.** The watchlist's per-symbol,
|
|
92
|
+
per-day price baseline is scratch. Routing it through a server-synced settings
|
|
93
|
+
blob is how a settings row becomes a cache.
|
|
94
|
+
- **`persistState` and `saveLayout` are two calls.** One records into the panel's
|
|
95
|
+
bag, the other flushes. Hiding a disk write inside "remember this" is exactly
|
|
96
|
+
the invisible coupling the port exists to remove.
|
|
97
|
+
|
|
98
|
+
## Everything a control shows is the host's
|
|
99
|
+
|
|
100
|
+
- **Text** arrives through `t()`. There is no dictionary here and no string the
|
|
101
|
+
package decides the wording of — see below for the keys you have to answer.
|
|
102
|
+
- **Colour** is class names the control emits; `styles/trading-controls.css`
|
|
103
|
+
gives them meaning and reads its values from `--t-*` custom properties. Four
|
|
104
|
+
names go the other way: `presentation.sideClass` and `pnlClass` are the
|
|
105
|
+
*host's* answer, and a control forwards the string to the cell without
|
|
106
|
+
looking at it. The shipped stylesheet paints `side-buy` / `side-sell` and
|
|
107
|
+
`pnl-positive` / `pnl-negative`; answer with those, or answer with your own
|
|
108
|
+
names and style those yourself. The port exports the list as
|
|
109
|
+
`PRESENTATION_CLASSES`, and `npm test` asserts the stylesheet styles all four.
|
|
110
|
+
- **Data** is handed in. A control never fetches on its own — except the two
|
|
111
|
+
reads the port names (`getExecutions`, `searchInstruments`), which the host
|
|
112
|
+
implements.
|
|
113
|
+
|
|
114
|
+
### The 52 keys a host has to answer
|
|
115
|
+
|
|
116
|
+
`t()` cannot fail. A key the host does not know is rendered to the user as
|
|
117
|
+
itself, so `NoActiveOrders` appears in the empty blotter and `ClosePanel`
|
|
118
|
+
becomes a tooltip — a missing translation looks like a typo, never like an
|
|
119
|
+
error. The complete list ships with the package:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import keys from '@stocksharp/trading-controls/translation-keys.json';
|
|
123
|
+
// { count: 52, keys: ['Actions', 'ActiveOrders', …] }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
It is **generated from the sources** (`npm run i18n:update`) and re-checked by
|
|
127
|
+
`npm test`, so it cannot drift from what the controls actually ask for.
|
|
128
|
+
|
|
129
|
+
The keys are not derivable, which is why the list is shipped rather than
|
|
130
|
+
described. Most are resource identifiers (`ClosePanel`, `ExportToExcel`,
|
|
131
|
+
`NoActiveOrders`, `Change24hPct`); some are English phrases, because that is the
|
|
132
|
+
form the terminal's dictionary already held them in (`Close position on {0}`,
|
|
133
|
+
`No trade history`, `Locked: ${0}`). `{0}`, `{1}` … are replaced positionally
|
|
134
|
+
from the `args` of the same `t()` call, so a translation may reorder them.
|
|
135
|
+
|
|
136
|
+
Controls render elements, never HTML strings, so nothing here can be an
|
|
137
|
+
injection site. A cell that holds a button holds a real element with its own
|
|
138
|
+
listener rather than an `onclick` attribute reaching a global.
|
|
139
|
+
|
|
140
|
+
## The stylesheet contract
|
|
141
|
+
|
|
142
|
+
The package **ships its stylesheet** — `@stocksharp/trading-controls/styles.css`.
|
|
143
|
+
Documenting the class names instead would have left an adopting page to
|
|
144
|
+
re-author about six hundred lines of CSS before it could see a table, which is
|
|
145
|
+
not "renders outside its original host" in any useful sense.
|
|
146
|
+
|
|
147
|
+
It is split in two so a host does not have to take a palette it disagrees with:
|
|
148
|
+
|
|
149
|
+
| file | what it is | when to import |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `@stocksharp/trading-controls/styles.css` | every rule, reading `var(--t-*)` and declaring none | always |
|
|
152
|
+
| `@stocksharp/trading-controls/theme.css` | a working dark + light palette (`:root`, and `:root[data-bs-theme="light"]`) | only if your page has no `--t-*` tokens of its own |
|
|
153
|
+
|
|
154
|
+
**The 22 properties a host must supply** if it skips `theme.css`:
|
|
155
|
+
|
|
156
|
+
| group | properties |
|
|
157
|
+
|---|---|
|
|
158
|
+
| surfaces | `--t-bg` `--t-panel` `--t-header` `--t-hover` `--t-border` |
|
|
159
|
+
| text | `--t-text` `--t-text-dim` `--t-text-bright` |
|
|
160
|
+
| accent | `--t-accent` `--t-accent-hover` `--t-accent-text` `--t-accent-glow-soft` |
|
|
161
|
+
| direction | `--t-green` `--t-red` `--t-green-flash` `--t-red-flash` |
|
|
162
|
+
| warning | `--t-orange` (destructive but not a cancel) `--t-warning` (read this) |
|
|
163
|
+
| type and shape | `--t-font` `--t-mono` `--t-radius` `--t-transition` |
|
|
164
|
+
|
|
165
|
+
All 22 are required — none of them has a fallback baked into the rule that
|
|
166
|
+
reads it. A `var(--t-orange, #f0b90b)` would keep the rule working on a host
|
|
167
|
+
that never declared the token, which means the host never finds out, and the
|
|
168
|
+
control quietly paints a shade from a palette nobody chose. `--t-orange` and
|
|
169
|
+
`--t-warning` were the two that did, and no longer do.
|
|
170
|
+
|
|
171
|
+
`npm test` runs `tools/check-style-contract.mjs`, which fails if a control emits
|
|
172
|
+
a class the stylesheet never styles, if the stylesheet misses one of the
|
|
173
|
+
`PRESENTATION_CLASSES` the host may return, if a rule reads a property
|
|
174
|
+
`theme.css` does not declare, if `theme.css` declares one no rule reads, or if
|
|
175
|
+
any rule reads a property with a fallback. The contract above is therefore
|
|
176
|
+
checked, not just written down.
|
|
177
|
+
|
|
178
|
+
Two things the page still owns:
|
|
179
|
+
|
|
180
|
+
- **Bootstrap Icons.** A control says which glyph a button wears (`bi bi-x`,
|
|
181
|
+
`bi bi-arrow-clockwise`) but does not draw it, exactly as it names a colour
|
|
182
|
+
token without defining it.
|
|
183
|
+
- **`grid-empty`, `visually-hidden`, `sort-asc` / `sort-desc`** come out of
|
|
184
|
+
`@stocksharp/grids`. This stylesheet carries them so an adopting page does not
|
|
185
|
+
have to know that; the second is spelled the way Bootstrap spells it.
|
|
186
|
+
|
|
187
|
+
## What each control is
|
|
188
|
+
|
|
189
|
+
### `ActiveOrdersWidget`
|
|
190
|
+
|
|
191
|
+
The session's whole order list — nothing drops out on its own, so a user can
|
|
192
|
+
watch an order's transitions instead of having a row vanish. A terminal row is
|
|
193
|
+
greyed; a rejected one carries its reason on a hover icon (unwrapped out of the
|
|
194
|
+
venue's JSON blob when it arrives that way) and its × dismisses locally rather
|
|
195
|
+
than sending a cancel with nothing to cancel. Quantity, price and stop are
|
|
196
|
+
editable in place while the venue still holds the order; committing an edit
|
|
197
|
+
restates the whole triple, because a replace is not a field patch.
|
|
198
|
+
|
|
199
|
+
### `PositionsWidget`
|
|
200
|
+
|
|
201
|
+
Alphabetical at rest — positions have no natural "newest". Cash sits above them
|
|
202
|
+
as a pinned row: a different shape from a position, so it supplies its own cells,
|
|
203
|
+
stays out of the sort and out of the exported sheet, and being content it
|
|
204
|
+
suppresses the "no positions" row. Close and reverse are per-row buttons wired to
|
|
205
|
+
deps.
|
|
206
|
+
|
|
207
|
+
### `TradeHistoryWidget`
|
|
208
|
+
|
|
209
|
+
Read-only, newest fill on top, loaded against whichever portfolio the host names
|
|
210
|
+
at refresh time rather than one captured at construction.
|
|
211
|
+
|
|
212
|
+
### `WatchlistWidget`
|
|
213
|
+
|
|
214
|
+
Live quotes with a search box, favourites and one tab per instrument category
|
|
215
|
+
found in the data. A quote patches the two affected cells through the grid's
|
|
216
|
+
`(rowKey, columnKey)` lookup instead of repainting — a repaint would cancel the
|
|
217
|
+
flash animation it just started. It paints a screenful (`RENDER_CAP`) while the
|
|
218
|
+
export and the subscription sync work over the whole filtered set, and only the
|
|
219
|
+
primary instance reports to the host's ticker.
|
|
220
|
+
|
|
221
|
+
## Exported sheets say what the screen says
|
|
222
|
+
|
|
223
|
+
Every blotter exports to `.xlsx` through the grid, and a column states both
|
|
224
|
+
forms: the localized text the user reads (`Sell`, `Filled`) and the raw figure
|
|
225
|
+
under it (`2000` rather than the formatted `2,000.00`), so a sheet is sortable as
|
|
226
|
+
numbers and cannot drift from the table it came from.
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
npm install
|
|
232
|
+
npm test # typecheck + public-API snapshot + style contract + unit tests
|
|
233
|
+
npm run build
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`dist/` and `tests/_dist/` are build outputs and are gitignored.
|
|
237
|
+
|
|
238
|
+
There is no browser in the test process: `tests/fake-dom.ts` implements the slice
|
|
239
|
+
of DOM the controls touch, and its size is the statement of how narrow that
|
|
240
|
+
slice is.
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Active orders — multi-instance.
|
|
2
|
+
//
|
|
3
|
+
// Builds its own DOM (see `_buildRoot`) rather than cloning a <template> out of
|
|
4
|
+
// the page, so it can be constructed by any host that supplies a `TradingHost`.
|
|
5
|
+
// Every instance shares the same data source (a host refresh / an order update
|
|
6
|
+
// off the socket); each update fans through the host's broadcast so duplicates
|
|
7
|
+
// stay in sync.
|
|
8
|
+
//
|
|
9
|
+
// The table itself is `DataGrid` from `@stocksharp/grids`: the columns below are
|
|
10
|
+
// the single declaration of this blotter — caption, value, rendering, colour
|
|
11
|
+
// class and export all live together, and the <thead> is left empty for the
|
|
12
|
+
// grid to fill. Cells that hold a control return a real Node with its own
|
|
13
|
+
// listener, so nothing here reaches a global through an inline onclick
|
|
14
|
+
// attribute; the callbacks arrive as deps instead.
|
|
15
|
+
import { cleanRejectReason, formatPrice, formatQty } from './formatters.js';
|
|
16
|
+
import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
|
|
17
|
+
import { ControlTypes } from './control-types.js';
|
|
18
|
+
import { assertHost } from './trading-host.js';
|
|
19
|
+
import { DataGrid } from '@stocksharp/grids/data-grid';
|
|
20
|
+
/// StockSharp order states as they arrive on the wire. The blotter branches on
|
|
21
|
+
/// them for row colour, for whether a cell can be edited in place and for what
|
|
22
|
+
/// the × button does, so they are named once here instead of appearing as bare
|
|
23
|
+
/// numbers at each branch.
|
|
24
|
+
export const OrderStates = {
|
|
25
|
+
PendingRisk: 1,
|
|
26
|
+
Sent: 2,
|
|
27
|
+
Active: 3,
|
|
28
|
+
PartiallyFilled: 4,
|
|
29
|
+
Filled: 5,
|
|
30
|
+
Rejected: 6,
|
|
31
|
+
Cancelled: 7,
|
|
32
|
+
};
|
|
33
|
+
export class ActiveOrdersWidget {
|
|
34
|
+
static create(hostEl, state, deps) {
|
|
35
|
+
// Assert before building: the markup below is localized through the
|
|
36
|
+
// host, so a missing host has to fail here rather than render a panel
|
|
37
|
+
// captioned with raw English keys.
|
|
38
|
+
const host = assertHost(deps?.host, 'ActiveOrdersWidget');
|
|
39
|
+
const root = ActiveOrdersWidget._buildRoot(host);
|
|
40
|
+
root.id = makePanelId(ActiveOrdersWidget.TYPE);
|
|
41
|
+
hostEl.appendChild(root);
|
|
42
|
+
return new ActiveOrdersWidget(root, state || {}, deps);
|
|
43
|
+
}
|
|
44
|
+
// The panel's markup. The host stylesheet reads this structure, and a
|
|
45
|
+
// docking host lifts `.panel-header`'s children into its tab strip.
|
|
46
|
+
//
|
|
47
|
+
// The rail's first two buttons are the ones that could not come across
|
|
48
|
+
// unchanged from the terminal's Razor template: they were inline
|
|
49
|
+
// `onclick="terminalApp.cancelAll()"` / `refreshOrders()`, references to a
|
|
50
|
+
// global this control must not have. They carry `.panel-cancel-all-btn` and
|
|
51
|
+
// `.panel-refresh-btn` now — the latter the name the other blotters' rails
|
|
52
|
+
// already give the identical button — and are wired to deps. Cancel-all
|
|
53
|
+
// keeps a distinct accessible name from its tooltip, which is why it states
|
|
54
|
+
// `aria-label` rather than taking the tooltip for both.
|
|
55
|
+
static _buildRoot(host) {
|
|
56
|
+
return makePanelRoot('active-orders-panel', host.t('ActiveOrders'), [
|
|
57
|
+
makeElement('div', 'panel-header', {}, [
|
|
58
|
+
makeElement('span', '', {}, [host.t('OpenOrders')]),
|
|
59
|
+
makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
|
|
60
|
+
]),
|
|
61
|
+
makeElement('div', 'panel-body panel-body-with-rail', {}, [
|
|
62
|
+
makeElement('div', 'panel-body-content', {}, [
|
|
63
|
+
makeElement('table', 'terminal-table active-orders-table', { role: 'table', 'aria-label': host.t('ActiveOrdersList') }, [
|
|
64
|
+
makeElement('thead', '', {}, []),
|
|
65
|
+
makeElement('tbody', 'active-orders-body', {}, []),
|
|
66
|
+
]),
|
|
67
|
+
]),
|
|
68
|
+
makeElement('div', 'panel-rail', { role: 'toolbar', 'aria-label': host.t('ActiveOrdersActions') }, [
|
|
69
|
+
makeIconButton('bt-icon-btn bt-icon-cancel-all panel-cancel-all-btn', host.t('CancelAll'), 'bi-x-circle', { 'aria-label': host.t('CancelAllOrders') }),
|
|
70
|
+
makeIconButton('bt-icon-btn panel-refresh-btn', host.t('Refresh'), 'bi-arrow-clockwise', {}),
|
|
71
|
+
makeIconButton('bt-icon-btn panel-export-btn', host.t('ExportToExcel'), 'bi-file-earmark-spreadsheet', {}),
|
|
72
|
+
]),
|
|
73
|
+
]),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
constructor(rootEl, _state, deps) {
|
|
77
|
+
this._host = assertHost(deps?.host, 'ActiveOrdersWidget');
|
|
78
|
+
for (const name of ['cancelOrder', 'dismissOrder', 'editOrderField', 'replaceOrder', 'cancelAllOrders', 'refreshOrders']) {
|
|
79
|
+
if (typeof deps?.[name] !== 'function')
|
|
80
|
+
throw new Error(`ActiveOrdersWidget: dep "${name}" is required`);
|
|
81
|
+
}
|
|
82
|
+
this.rootEl = rootEl;
|
|
83
|
+
this._deps = deps;
|
|
84
|
+
this.el = this.rootEl.querySelector('.active-orders-body');
|
|
85
|
+
this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
|
|
86
|
+
this._cancelAllBtn = this.rootEl.querySelector('.panel-cancel-all-btn');
|
|
87
|
+
this._refreshBtn = this.rootEl.querySelector('.panel-refresh-btn');
|
|
88
|
+
this._exportBtn = this.rootEl.querySelector('.panel-export-btn');
|
|
89
|
+
this._orders = [];
|
|
90
|
+
this._closeBtn?.addEventListener('click', (e) => {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
this._host.close();
|
|
93
|
+
});
|
|
94
|
+
this._cancelAllBtn?.addEventListener('click', (e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
this._deps.cancelAllOrders();
|
|
97
|
+
});
|
|
98
|
+
this._refreshBtn?.addEventListener('click', (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
this._deps.refreshOrders();
|
|
101
|
+
});
|
|
102
|
+
this._exportBtn?.addEventListener('click', (e) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
this._export();
|
|
105
|
+
});
|
|
106
|
+
const head = this.rootEl.querySelector('.active-orders-table thead');
|
|
107
|
+
this._grid = head && this.el
|
|
108
|
+
? new DataGrid({
|
|
109
|
+
head: head,
|
|
110
|
+
body: this.el,
|
|
111
|
+
columns: this._columns(),
|
|
112
|
+
// Newest order on top by default (localId is monotonic) — the resting view puts the
|
|
113
|
+
// user's most recent actions where they look, instead of raw cache order. Natural
|
|
114
|
+
// array order is "newest first" too (deltas unshift), but only by accident; any
|
|
115
|
+
// header click re-renders the same array through the sorter.
|
|
116
|
+
defaultSort: { col: 'id', dir: 'desc' },
|
|
117
|
+
// The register tx, not the friendly localId shown in the ID column: it is what
|
|
118
|
+
// cancel / replace / inline edit address an order by.
|
|
119
|
+
rowKey: (o) => String(o.id),
|
|
120
|
+
emptyText: this._host.t('NoActiveOrders'),
|
|
121
|
+
// Terminal rows are greyed out so the live ones stay visually prominent.
|
|
122
|
+
rowClass: (o) => ActiveOrdersWidget._rowClass(o),
|
|
123
|
+
})
|
|
124
|
+
: null;
|
|
125
|
+
this._host.register(this);
|
|
126
|
+
}
|
|
127
|
+
dispose() {
|
|
128
|
+
this._host.unregister(this);
|
|
129
|
+
try {
|
|
130
|
+
this.rootEl.remove();
|
|
131
|
+
}
|
|
132
|
+
catch { /* already detached */ }
|
|
133
|
+
}
|
|
134
|
+
update(orders) {
|
|
135
|
+
this._orders = orders || [];
|
|
136
|
+
this._grid?.setRows(this._orders);
|
|
137
|
+
}
|
|
138
|
+
// Export the table to .xlsx in the currently rendered (sorted) order, with the
|
|
139
|
+
// same localized side/type/status texts the user sees on screen — the columns
|
|
140
|
+
// carry both forms, so the sheet cannot drift from the table.
|
|
141
|
+
_export() {
|
|
142
|
+
this._grid?.download('orders', this._host.t('OpenOrders'));
|
|
143
|
+
}
|
|
144
|
+
/// Apply a single order delta from the feed. Insert if new, merge if known.
|
|
145
|
+
/// Nothing drops out of the list: Filled and Cancelled rows stay so the user
|
|
146
|
+
/// can watch an order's transitions instead of having a row vanish, and a
|
|
147
|
+
/// Rejected row stays so its reason can be read on hover and dismissed
|
|
148
|
+
/// explicitly through the × button.
|
|
149
|
+
applyDelta(order) {
|
|
150
|
+
if (!order || order.id == null)
|
|
151
|
+
return;
|
|
152
|
+
const idx = this._orders.findIndex(o => o.id === order.id);
|
|
153
|
+
if (idx >= 0)
|
|
154
|
+
this._orders[idx] = { ...this._orders[idx], ...order };
|
|
155
|
+
else
|
|
156
|
+
this._orders.unshift(order);
|
|
157
|
+
this.update(this._orders);
|
|
158
|
+
}
|
|
159
|
+
/// Remove a row from the local view without touching the server. Used by the
|
|
160
|
+
/// × button on a terminal order (nothing left to cancel), and in any other
|
|
161
|
+
/// client-side dismiss path.
|
|
162
|
+
removeOrder(orderId) {
|
|
163
|
+
const idx = this._orders.findIndex(o => o.id === orderId);
|
|
164
|
+
if (idx >= 0) {
|
|
165
|
+
this._orders.splice(idx, 1);
|
|
166
|
+
this.update(this._orders);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
getOrder(orderId) {
|
|
170
|
+
return this._orders.find(o => o.id === orderId);
|
|
171
|
+
}
|
|
172
|
+
startInlineEdit(orderId, field) {
|
|
173
|
+
const order = this.getOrder(orderId);
|
|
174
|
+
if (!order)
|
|
175
|
+
return;
|
|
176
|
+
// Addressed by the grid's row key (the register tx) and the column key, so
|
|
177
|
+
// the edit lands on the right cell whatever the sort is. A lookup that
|
|
178
|
+
// scanned the rendered ID column for the order id would never match, because
|
|
179
|
+
// that column shows the friendly counter.
|
|
180
|
+
const cell = this._grid?.cellElement(String(orderId), field);
|
|
181
|
+
if (!cell || cell.querySelector('input'))
|
|
182
|
+
return;
|
|
183
|
+
const currentValue = order[field];
|
|
184
|
+
const input = document.createElement('input');
|
|
185
|
+
input.type = 'number';
|
|
186
|
+
input.className = 'inline-edit-input';
|
|
187
|
+
input.value = String(currentValue ?? '');
|
|
188
|
+
input.step = field === 'quantity' ? '1' : '0.01';
|
|
189
|
+
input.min = field === 'quantity' ? '1' : '0.01';
|
|
190
|
+
cell.textContent = '';
|
|
191
|
+
cell.appendChild(input);
|
|
192
|
+
input.focus();
|
|
193
|
+
input.select();
|
|
194
|
+
const commit = () => {
|
|
195
|
+
const newVal = parseFloat(input.value);
|
|
196
|
+
if (isNaN(newVal) || newVal <= 0) {
|
|
197
|
+
this.update(this._orders);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (newVal === currentValue) {
|
|
201
|
+
this.update(this._orders);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const updated = { quantity: order.quantity, limitPrice: order.limitPrice, stopPrice: order.stopPrice };
|
|
205
|
+
updated[field] = newVal;
|
|
206
|
+
this._deps.replaceOrder(orderId, updated.quantity, updated.limitPrice, updated.stopPrice);
|
|
207
|
+
};
|
|
208
|
+
input.addEventListener('blur', commit);
|
|
209
|
+
input.addEventListener('keydown', (e) => {
|
|
210
|
+
if (e.key === 'Enter') {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
input.blur();
|
|
213
|
+
}
|
|
214
|
+
if (e.key === 'Escape') {
|
|
215
|
+
this.update(this._orders);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// The blotter's single column declaration: what the header says, what the
|
|
220
|
+
// grid sorts on, what the cell shows, how it is coloured, and what reaches
|
|
221
|
+
// the exported sheet.
|
|
222
|
+
_columns() {
|
|
223
|
+
// `label`, not `t` — every row lambda below binds `o` for the order,
|
|
224
|
+
// but keeping the translator under a name of its own reads clearer
|
|
225
|
+
// next to the presentation object it sits beside.
|
|
226
|
+
const label = (key) => this._host.t(key);
|
|
227
|
+
const presentation = this._host.presentation;
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
key: 'id',
|
|
231
|
+
header: label('ID'),
|
|
232
|
+
exportable: true,
|
|
233
|
+
// Per-user friendly counter (1, 2, 3, …) issued by the host. `o.id`
|
|
234
|
+
// keeps the register tx for cancel / replace routing; `o.localId` is
|
|
235
|
+
// just the badge the user wants to see. Rows cached from before the
|
|
236
|
+
// counter shipped have no localId — fall back to id so they don't go
|
|
237
|
+
// blank until the next snapshot replay reassigns one.
|
|
238
|
+
value: (o) => o.localId ?? o.id,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: 'instrument',
|
|
242
|
+
header: label('Sym'),
|
|
243
|
+
exportable: true,
|
|
244
|
+
value: (o) => o.instrument,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
key: 'side',
|
|
248
|
+
header: label('Side'),
|
|
249
|
+
exportable: true,
|
|
250
|
+
value: (o) => o.side,
|
|
251
|
+
render: (o) => presentation.sideText(o.side),
|
|
252
|
+
cellClass: (o) => presentation.sideClass(o.side),
|
|
253
|
+
exportValue: (o) => presentation.sideText(o.side),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: 'type',
|
|
257
|
+
header: label('Type'),
|
|
258
|
+
exportable: true,
|
|
259
|
+
value: (o) => o.type,
|
|
260
|
+
render: (o) => presentation.typeText(o.type, o.limitPrice, o.stopPrice),
|
|
261
|
+
exportValue: (o) => presentation.typeText(o.type, o.limitPrice, o.stopPrice),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
key: 'quantity',
|
|
265
|
+
header: label('Qty'),
|
|
266
|
+
exportable: true,
|
|
267
|
+
value: (o) => o.quantity,
|
|
268
|
+
render: (o) => formatQty(o.quantity),
|
|
269
|
+
cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'quantity'),
|
|
270
|
+
bindCell: (td, o) => this._bindInlineEdit(td, o, 'quantity'),
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
key: 'limitPrice',
|
|
274
|
+
header: label('Price'),
|
|
275
|
+
exportable: true,
|
|
276
|
+
value: (o) => o.limitPrice,
|
|
277
|
+
render: (o) => o.limitPrice ? formatPrice(o.limitPrice) : label('MKT'),
|
|
278
|
+
cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'limitPrice'),
|
|
279
|
+
bindCell: (td, o) => this._bindInlineEdit(td, o, 'limitPrice'),
|
|
280
|
+
exportValue: (o) => o.limitPrice ?? label('MKT'),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: 'stopPrice',
|
|
284
|
+
header: label('Stop'),
|
|
285
|
+
exportable: true,
|
|
286
|
+
value: (o) => o.stopPrice,
|
|
287
|
+
render: (o) => o.stopPrice ? formatPrice(o.stopPrice) : '--',
|
|
288
|
+
cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'stopPrice'),
|
|
289
|
+
bindCell: (td, o) => this._bindInlineEdit(td, o, 'stopPrice'),
|
|
290
|
+
exportValue: (o) => o.stopPrice ?? '',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
key: 'status',
|
|
294
|
+
header: label('Status'),
|
|
295
|
+
exportable: true,
|
|
296
|
+
value: (o) => o.status,
|
|
297
|
+
render: (o) => this._statusCell(o),
|
|
298
|
+
exportValue: (o) => presentation.statusText(o.status),
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
key: 'actions',
|
|
302
|
+
header: label('Actions'),
|
|
303
|
+
headerHidden: true,
|
|
304
|
+
exportable: false,
|
|
305
|
+
cellClass: () => 'position-actions',
|
|
306
|
+
render: (o) => this._actionButton(o),
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
static _rowClass(order) {
|
|
311
|
+
if (order.status === OrderStates.Filled)
|
|
312
|
+
return 'order-filled';
|
|
313
|
+
if (order.status === OrderStates.Rejected)
|
|
314
|
+
return 'order-rejected';
|
|
315
|
+
if (order.status === OrderStates.Cancelled)
|
|
316
|
+
return 'order-cancelled';
|
|
317
|
+
return '';
|
|
318
|
+
}
|
|
319
|
+
// An order can still be modified while the venue holds it. A market order has
|
|
320
|
+
// no limit price and an unconditional order no stop price, so those cells have
|
|
321
|
+
// nothing to edit even then.
|
|
322
|
+
static _canEdit(order, field) {
|
|
323
|
+
const modifiable = order.status === OrderStates.Active || order.status === OrderStates.Sent;
|
|
324
|
+
return modifiable && (field === 'quantity' || !!order[field]);
|
|
325
|
+
}
|
|
326
|
+
static _editableClass(order, field) {
|
|
327
|
+
return ActiveOrdersWidget._canEdit(order, field) ? 'cell-editable' : '';
|
|
328
|
+
}
|
|
329
|
+
// Double-click to edit belongs to the whole cell rather than to a control
|
|
330
|
+
// inside it, which is why it is wired here instead of returned by render().
|
|
331
|
+
_bindInlineEdit(td, order, field) {
|
|
332
|
+
if (!ActiveOrdersWidget._canEdit(order, field))
|
|
333
|
+
return;
|
|
334
|
+
td.addEventListener('dblclick', () => this._deps.editOrderField(order.id, field));
|
|
335
|
+
}
|
|
336
|
+
// Status text, plus a hoverable icon carrying the rejection reason. Some
|
|
337
|
+
// venues wrap the human-readable rejection inside a JSON blob;
|
|
338
|
+
// cleanRejectReason extracts `.message` and falls back to the raw string.
|
|
339
|
+
// The reason goes on the element's title property, so no quote escaping is
|
|
340
|
+
// involved the way it was when this row was built as markup.
|
|
341
|
+
_statusCell(order) {
|
|
342
|
+
const text = this._host.presentation.statusText(order.status);
|
|
343
|
+
const reason = order.status === OrderStates.Rejected ? cleanRejectReason(order.rejectReason) : '';
|
|
344
|
+
if (!reason)
|
|
345
|
+
return text;
|
|
346
|
+
const cell = document.createDocumentFragment();
|
|
347
|
+
cell.appendChild(document.createTextNode(text + ' '));
|
|
348
|
+
const icon = document.createElement('i');
|
|
349
|
+
icon.className = 'bi bi-question-circle reject-reason-icon';
|
|
350
|
+
icon.title = reason;
|
|
351
|
+
cell.appendChild(icon);
|
|
352
|
+
return cell;
|
|
353
|
+
}
|
|
354
|
+
// The × button, wired to the controller through deps. Status decides what it
|
|
355
|
+
// means: Pending / Active is a real cancel over the wire, while Filled /
|
|
356
|
+
// Rejected / Cancelled have nothing left to cancel — the row is history kept
|
|
357
|
+
// around so the user can read its final state, so the button dismisses it
|
|
358
|
+
// locally.
|
|
359
|
+
_actionButton(order) {
|
|
360
|
+
const isTerminal = order.status === OrderStates.Filled
|
|
361
|
+
|| order.status === OrderStates.Rejected
|
|
362
|
+
|| order.status === OrderStates.Cancelled;
|
|
363
|
+
const title = isTerminal ? this._host.t('Dismiss') : this._host.t('Cancel order');
|
|
364
|
+
const button = document.createElement('button');
|
|
365
|
+
button.type = 'button';
|
|
366
|
+
button.className = 'bt-icon-btn bt-icon-cancel';
|
|
367
|
+
button.title = title;
|
|
368
|
+
button.setAttribute('aria-label', `${title} #${order.id}`);
|
|
369
|
+
const icon = document.createElement('i');
|
|
370
|
+
icon.className = 'bi bi-x-circle';
|
|
371
|
+
button.appendChild(icon);
|
|
372
|
+
button.addEventListener('click', () => {
|
|
373
|
+
if (isTerminal)
|
|
374
|
+
this._deps.dismissOrder(order.id);
|
|
375
|
+
else
|
|
376
|
+
this._deps.cancelOrder(order.id);
|
|
377
|
+
});
|
|
378
|
+
return button;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
ActiveOrdersWidget.TYPE = ControlTypes.ActiveOrders;
|
|
382
|
+
//# sourceMappingURL=active-orders-widget.js.map
|