@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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// The host port — the whole of what a trading control needs from whoever
|
|
2
|
+
// embeds it.
|
|
3
|
+
//
|
|
4
|
+
// A control is written against this interface and nothing else: it never
|
|
5
|
+
// imports a translator, a preference singleton, a panel registry or a docking
|
|
6
|
+
// manager, and it never reaches a window global. A host — a terminal page, a
|
|
7
|
+
// backoffice screen, a demo harness — supplies one object shaped like
|
|
8
|
+
// `TradingHost` per control instance, and that object is the only way out of
|
|
9
|
+
// the control.
|
|
10
|
+
//
|
|
11
|
+
// This module imports nothing but the row shapes it names. It is the contract,
|
|
12
|
+
// so it must be readable by a host that shares none of the package's internals.
|
|
13
|
+
//
|
|
14
|
+
// Everything here is REQUIRED, and `assertHost` proves it at construction.
|
|
15
|
+
// There is no optional member and no default, because a half-supplied host is
|
|
16
|
+
// how a control half-works: it renders, it looks alive, and the one capability
|
|
17
|
+
// nobody wired is discovered by a user clicking something that does nothing.
|
|
18
|
+
// A missing member has to be a loud failure at wiring time, not a quiet one at
|
|
19
|
+
// click time.
|
|
20
|
+
//
|
|
21
|
+
// Seven members have no call site among the four controls this package ships —
|
|
22
|
+
// `spawn`, `persistState`, `saveLayout`, `log`, `trading.pickInstrument`,
|
|
23
|
+
// `marketData.resubscribe` and `marketData.getOrders`. They are required
|
|
24
|
+
// anyway, and the reason is not a future one:
|
|
25
|
+
//
|
|
26
|
+
// The port was extracted from a terminal that has SEVEN controls, four of
|
|
27
|
+
// which now live here. The other three — the order book, the order entry
|
|
28
|
+
// pad and the trade feed — are already written against THIS interface, in
|
|
29
|
+
// this repository's sibling host, importing `TradingHost` and `assertHost`
|
|
30
|
+
// from this module today. Every one of the seven members above is called by
|
|
31
|
+
// them: the order book alone accounts for `resubscribe` (recovering from a
|
|
32
|
+
// sequence gap without unsubscribing its neighbours), `getOrders` (its "your
|
|
33
|
+
// size is here" badge), `log` (wire anomalies the user cannot see) and
|
|
34
|
+
// `pickInstrument`; the order book and the trade feed both `spawn` and both
|
|
35
|
+
// `persistState` + `saveLayout`.
|
|
36
|
+
//
|
|
37
|
+
// So this is one port with three of its consumers still on the host's side
|
|
38
|
+
// of the boundary, not a port padded with speculation. Narrowing it now and
|
|
39
|
+
// widening it again as each of those three moves in would break every host
|
|
40
|
+
// twice for the sake of a window in which no control is missing.
|
|
41
|
+
//
|
|
42
|
+
// The cost is real and worth stating: a host adopting only the four shipped
|
|
43
|
+
// controls must still supply seven members nothing will call. They are cheap
|
|
44
|
+
// (a no-op `spawn`, a `log` that forwards to the console) — but they are not
|
|
45
|
+
// free, and an adopter who supplies stubs is not doing anything wrong.
|
|
46
|
+
import type { InstrumentRow, OrderRow, OrderSide, OrderStatus, OrderType, QuoteStats, TradeRow } from './trading-data.js';
|
|
47
|
+
|
|
48
|
+
/// How a host words and colours the trading vocabulary. The control owns the
|
|
49
|
+
/// data; the host owns the language and the stylesheet, so it says what a side
|
|
50
|
+
/// or a status reads as and which CSS class carries its colour.
|
|
51
|
+
///
|
|
52
|
+
/// The two class methods are the one place a host has to agree with the shipped
|
|
53
|
+
/// stylesheet rather than merely supply values to it. A control puts whatever
|
|
54
|
+
/// string comes back straight onto the cell without inspecting it, so nothing
|
|
55
|
+
/// in `src/` ever emits these names and `styles/trading-controls.css` cannot be
|
|
56
|
+
/// read to discover them. They are named here, and `PRESENTATION_CLASSES` below
|
|
57
|
+
/// is the same list as data — `tools/check-style-contract.mjs` asserts the
|
|
58
|
+
/// stylesheet styles every one of them, so the agreement is checked rather than
|
|
59
|
+
/// discovered by a cell that came out unstyled.
|
|
60
|
+
export interface TradingPresentation {
|
|
61
|
+
/// Localized "Buy" / "Sell".
|
|
62
|
+
sideText(side: OrderSide): string;
|
|
63
|
+
/// Localized short order type — "LMT", "MKT", "STP", "STP-LMT". Needs both
|
|
64
|
+
/// prices because a conditional order is only a stop-limit when it carries
|
|
65
|
+
/// a limit price as well as a stop price.
|
|
66
|
+
typeText(type: OrderType, limitPrice: number, stopPrice: number): string;
|
|
67
|
+
/// Localized order status.
|
|
68
|
+
statusText(status: OrderStatus): string;
|
|
69
|
+
/// CSS class colouring a cell by side. The shipped stylesheet styles
|
|
70
|
+
/// `side-buy` and `side-sell`; a host on its own palette may answer with
|
|
71
|
+
/// its own names and style those instead.
|
|
72
|
+
sideClass(side: OrderSide): string;
|
|
73
|
+
/// CSS class colouring a cell by the sign of a P&L figure. The shipped
|
|
74
|
+
/// stylesheet styles `pnl-positive` and `pnl-negative`; the empty string is
|
|
75
|
+
/// a legitimate answer for "no colour".
|
|
76
|
+
pnlClass(pnl: number): string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// The class names `TradingPresentation` is expected to return, as data.
|
|
80
|
+
///
|
|
81
|
+
/// Exported because it is a contract, not a detail: a host writing its own
|
|
82
|
+
/// presentation reads this to know which names the shipped stylesheet already
|
|
83
|
+
/// paints, and the style-contract check reads it to prove they are all styled.
|
|
84
|
+
export const PRESENTATION_CLASSES = {
|
|
85
|
+
sideClass: ['side-buy', 'side-sell'],
|
|
86
|
+
pnlClass: ['pnl-positive', 'pnl-negative'],
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
/// A string-keyed store. Two of these reach a control and they are NOT
|
|
90
|
+
/// interchangeable — see `TradingHost.preferences` and `TradingHost.cache`.
|
|
91
|
+
export interface HostStore {
|
|
92
|
+
/// The stored value, or `fallback` when the key was never written.
|
|
93
|
+
get(key: string, fallback: string | null): string | null;
|
|
94
|
+
/// Write, or clear by writing null.
|
|
95
|
+
set(key: string, value: string | null): void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The read side of the trading account, as far as a control is concerned.
|
|
99
|
+
/// Narrow on purpose: a control asks for the few calls it makes rather than
|
|
100
|
+
/// being handed a whole API client to explore.
|
|
101
|
+
export interface TradingApi {
|
|
102
|
+
/// Executed trades, newest first, for one portfolio. A null `symbol` means
|
|
103
|
+
/// every instrument — the argument is stated either way rather than being
|
|
104
|
+
/// left off, so a call site says which of the two it wants.
|
|
105
|
+
getExecutions(portfolioId: number, symbol: string | null, limit: number): Promise<TradeRow[]>;
|
|
106
|
+
/// Instruments matching a query; the empty query means "everything".
|
|
107
|
+
searchInstruments(query: string): Promise<InstrumentRow[]>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// How much of a symbol a control wants streamed. A quote row needs the last
|
|
111
|
+
/// price, a tape needs every print, a ladder needs the book as well — and each
|
|
112
|
+
/// costs the wire something different, so a control states which it is asking
|
|
113
|
+
/// for rather than leaving the argument off and taking a default.
|
|
114
|
+
export const MarketDataLevels = {
|
|
115
|
+
/// Level 1 only: best bid/ask and last price.
|
|
116
|
+
Quotes: 'l1only',
|
|
117
|
+
/// Level 1 plus every tick.
|
|
118
|
+
Tape: 'tape',
|
|
119
|
+
/// Level 1, ticks and market depth.
|
|
120
|
+
Full: 'full',
|
|
121
|
+
} as const;
|
|
122
|
+
|
|
123
|
+
export type MarketDataLevel = typeof MarketDataLevels[keyof typeof MarketDataLevels];
|
|
124
|
+
|
|
125
|
+
/// The live market-data connection. `addSymbol` / `removeSymbol` are
|
|
126
|
+
/// refcounted by the implementation, so two controls watching one symbol hold
|
|
127
|
+
/// one subscription between them and neither can unsubscribe the other.
|
|
128
|
+
export interface MarketDataClient {
|
|
129
|
+
addSymbol(symbol: string, level: MarketDataLevel): Promise<unknown>;
|
|
130
|
+
removeSymbol(symbol: string): Promise<unknown>;
|
|
131
|
+
/// Make the server resend `symbol` from scratch, without touching the
|
|
132
|
+
/// refcount. A control applying diffs against a sequence cannot recover
|
|
133
|
+
/// from a missed frame on its own — the only cure is a fresh snapshot, and
|
|
134
|
+
/// asking for one must not unsubscribe the other controls watching the
|
|
135
|
+
/// same symbol.
|
|
136
|
+
resubscribe(symbol: string, level: MarketDataLevel): Promise<void>;
|
|
137
|
+
/// The client's cache of this session's own orders. The order book paints
|
|
138
|
+
/// its "you have size here" badge from this rather than subscribing again.
|
|
139
|
+
getOrders(): OrderRow[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// What a control reads about the account and the instrument universe.
|
|
143
|
+
export interface TradingContext {
|
|
144
|
+
readonly api: TradingApi;
|
|
145
|
+
readonly marketData: MarketDataClient;
|
|
146
|
+
/// The portfolio a control should load against, read per use rather than
|
|
147
|
+
/// captured — the user can switch portfolio under a control that is
|
|
148
|
+
/// already on screen.
|
|
149
|
+
portfolioId(): number | null;
|
|
150
|
+
/// Ask the host to let the user pick an instrument. The host owns the
|
|
151
|
+
/// picker UI; the control only learns what was chosen, and learns nothing
|
|
152
|
+
/// if the user dismisses it.
|
|
153
|
+
pickInstrument(onPicked: (symbol: string) => void): void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Where a control's summary of what it is showing goes. A status-bar ticker is
|
|
157
|
+
/// page chrome outside any control, so a control reports and the host decides
|
|
158
|
+
/// what to do with the report.
|
|
159
|
+
export interface TickerSink {
|
|
160
|
+
publish(symbols: string[], stats: Map<string, QuoteStats>): void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// One live control instance, from the host's side. Deliberately opaque: the
|
|
164
|
+
/// host holds a heterogeneous set and only ever hands instances back to the
|
|
165
|
+
/// control that asked for them, which is what `broadcast<T>` expresses.
|
|
166
|
+
export type TradingControl = object;
|
|
167
|
+
|
|
168
|
+
/// Everything a trading control needs from its host.
|
|
169
|
+
export interface TradingHost {
|
|
170
|
+
// -- this instance's role on the page ---------------------------------
|
|
171
|
+
/// True when this instance is the one that speaks for the page. A page has
|
|
172
|
+
/// one status-bar ticker and a control can be duplicated, so somebody has
|
|
173
|
+
/// to decide which duplicate feeds it — and that is host policy, not
|
|
174
|
+
/// something a control can work out about itself.
|
|
175
|
+
readonly isPrimary: boolean;
|
|
176
|
+
|
|
177
|
+
// -- language and wording ---------------------------------------------
|
|
178
|
+
/// Translate. `key` is the English source text; `{0}`, `{1}` … in the
|
|
179
|
+
/// translation are replaced positionally from `args`.
|
|
180
|
+
t(key: string, ...args: unknown[]): string;
|
|
181
|
+
readonly presentation: TradingPresentation;
|
|
182
|
+
|
|
183
|
+
// -- storage ------------------------------------------------------------
|
|
184
|
+
/// Settings that belong to the user and are expected to survive — a chosen
|
|
185
|
+
/// depth, a view mode, a favourites list. May be slow and may be shared
|
|
186
|
+
/// across devices.
|
|
187
|
+
readonly preferences: HostStore;
|
|
188
|
+
/// Local scratch that may be thrown away — a per-day price baseline, a
|
|
189
|
+
/// memoised computation. Deliberately a different store from
|
|
190
|
+
/// `preferences`: writing high-churn per-symbol keys into a synced
|
|
191
|
+
/// per-user settings blob is how a settings row becomes a cache.
|
|
192
|
+
readonly cache: HostStore;
|
|
193
|
+
|
|
194
|
+
// -- the trading context it reads ---------------------------------------
|
|
195
|
+
readonly trading: TradingContext;
|
|
196
|
+
|
|
197
|
+
// -- where its ticker updates go ----------------------------------------
|
|
198
|
+
readonly ticker: TickerSink;
|
|
199
|
+
|
|
200
|
+
// -- permission and diagnostics -----------------------------------------
|
|
201
|
+
/// May the user do this? `action` is the English name of the attempt
|
|
202
|
+
/// ("add to favorites"); the host translates it and may show a sign-in
|
|
203
|
+
/// prompt before answering false.
|
|
204
|
+
allow(action: string): boolean;
|
|
205
|
+
/// Send a diagnostic line wherever this host collects them. Controls use
|
|
206
|
+
/// it for wire anomalies the user cannot see and support has to.
|
|
207
|
+
log(message: string): void;
|
|
208
|
+
|
|
209
|
+
// -- lifecycle calls it makes back --------------------------------------
|
|
210
|
+
/// This control wants to go away.
|
|
211
|
+
close(): void;
|
|
212
|
+
/// The `+` gesture: another control of the same kind, starting from
|
|
213
|
+
/// `state`. The host knows what kind this one is, so the control does not
|
|
214
|
+
/// name its own type.
|
|
215
|
+
spawn(state: Record<string, unknown>): void;
|
|
216
|
+
/// Remember this control's per-instance state. Recording only — call
|
|
217
|
+
/// `saveLayout` when the change is worth flushing.
|
|
218
|
+
persistState(patch: Record<string, unknown>): void;
|
|
219
|
+
/// Write the layout out now, bypassing any debounce the host applies.
|
|
220
|
+
saveLayout(): void;
|
|
221
|
+
|
|
222
|
+
// -- the host's handle on live instances --------------------------------
|
|
223
|
+
/// Announce this instance. A host fans incoming data to every live control
|
|
224
|
+
/// of a kind, so an unregistered control is a control that never updates.
|
|
225
|
+
register(control: TradingControl): void;
|
|
226
|
+
unregister(control: TradingControl): void;
|
|
227
|
+
/// Apply something to every live control of this kind, this one included.
|
|
228
|
+
/// The caller knows what its own siblings are, which is why `T` is its to
|
|
229
|
+
/// name.
|
|
230
|
+
broadcast<T>(apply: (control: T) => void): void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Prove a host object satisfies the port, and say precisely what is missing
|
|
234
|
+
/// when it does not.
|
|
235
|
+
///
|
|
236
|
+
/// Called from every control's constructor before anything else, so a
|
|
237
|
+
/// mis-wired host fails at construction with the member named, instead of at
|
|
238
|
+
/// the first click that needed it.
|
|
239
|
+
export function assertHost(host: TradingHost, controlName: string): TradingHost {
|
|
240
|
+
_required(host, 'host', 'object', controlName);
|
|
241
|
+
|
|
242
|
+
_required(host.isPrimary, 'host.isPrimary', 'boolean', controlName);
|
|
243
|
+
_required(host.t, 'host.t', 'function', controlName);
|
|
244
|
+
|
|
245
|
+
_required(host.presentation, 'host.presentation', 'object', controlName);
|
|
246
|
+
_required(host.presentation.sideText, 'host.presentation.sideText', 'function', controlName);
|
|
247
|
+
_required(host.presentation.typeText, 'host.presentation.typeText', 'function', controlName);
|
|
248
|
+
_required(host.presentation.statusText, 'host.presentation.statusText', 'function', controlName);
|
|
249
|
+
_required(host.presentation.sideClass, 'host.presentation.sideClass', 'function', controlName);
|
|
250
|
+
_required(host.presentation.pnlClass, 'host.presentation.pnlClass', 'function', controlName);
|
|
251
|
+
|
|
252
|
+
_required(host.preferences, 'host.preferences', 'object', controlName);
|
|
253
|
+
_required(host.preferences.get, 'host.preferences.get', 'function', controlName);
|
|
254
|
+
_required(host.preferences.set, 'host.preferences.set', 'function', controlName);
|
|
255
|
+
|
|
256
|
+
_required(host.cache, 'host.cache', 'object', controlName);
|
|
257
|
+
_required(host.cache.get, 'host.cache.get', 'function', controlName);
|
|
258
|
+
_required(host.cache.set, 'host.cache.set', 'function', controlName);
|
|
259
|
+
|
|
260
|
+
_required(host.trading, 'host.trading', 'object', controlName);
|
|
261
|
+
_required(host.trading.api, 'host.trading.api', 'object', controlName);
|
|
262
|
+
// The calls, not just the objects holding them. An `api: {}` is the shape a
|
|
263
|
+
// half-wired host actually has — the object gets built, the methods get
|
|
264
|
+
// forgotten — so stopping at the object would skip exactly the members this
|
|
265
|
+
// function exists to catch.
|
|
266
|
+
_required(host.trading.api.getExecutions, 'host.trading.api.getExecutions', 'function', controlName);
|
|
267
|
+
_required(host.trading.api.searchInstruments, 'host.trading.api.searchInstruments', 'function', controlName);
|
|
268
|
+
_required(host.trading.marketData, 'host.trading.marketData', 'object', controlName);
|
|
269
|
+
_required(host.trading.marketData.addSymbol, 'host.trading.marketData.addSymbol', 'function', controlName);
|
|
270
|
+
_required(host.trading.marketData.removeSymbol, 'host.trading.marketData.removeSymbol', 'function', controlName);
|
|
271
|
+
_required(host.trading.marketData.resubscribe, 'host.trading.marketData.resubscribe', 'function', controlName);
|
|
272
|
+
_required(host.trading.marketData.getOrders, 'host.trading.marketData.getOrders', 'function', controlName);
|
|
273
|
+
_required(host.trading.portfolioId, 'host.trading.portfolioId', 'function', controlName);
|
|
274
|
+
_required(host.trading.pickInstrument, 'host.trading.pickInstrument', 'function', controlName);
|
|
275
|
+
|
|
276
|
+
_required(host.ticker, 'host.ticker', 'object', controlName);
|
|
277
|
+
_required(host.ticker.publish, 'host.ticker.publish', 'function', controlName);
|
|
278
|
+
|
|
279
|
+
_required(host.allow, 'host.allow', 'function', controlName);
|
|
280
|
+
_required(host.log, 'host.log', 'function', controlName);
|
|
281
|
+
|
|
282
|
+
_required(host.close, 'host.close', 'function', controlName);
|
|
283
|
+
_required(host.spawn, 'host.spawn', 'function', controlName);
|
|
284
|
+
_required(host.persistState, 'host.persistState', 'function', controlName);
|
|
285
|
+
_required(host.saveLayout, 'host.saveLayout', 'function', controlName);
|
|
286
|
+
|
|
287
|
+
_required(host.register, 'host.register', 'function', controlName);
|
|
288
|
+
_required(host.unregister, 'host.unregister', 'function', controlName);
|
|
289
|
+
_required(host.broadcast, 'host.broadcast', 'function', controlName);
|
|
290
|
+
|
|
291
|
+
return host;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// `typeof null` is 'object', which would let a null store through the object
|
|
295
|
+
// checks — the one case worth spelling out, because null is exactly what an
|
|
296
|
+
// unwired dependency looks like.
|
|
297
|
+
function _required(value: unknown, path: string, kind: string, controlName: string): void {
|
|
298
|
+
if (value === null || value === undefined || typeof value !== kind)
|
|
299
|
+
throw new Error(`${controlName}: ${path} is required`);
|
|
300
|
+
}
|