@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.
Files changed (59) hide show
  1. package/LICENSE +30 -0
  2. package/NOTICE +21 -0
  3. package/README.md +240 -0
  4. package/dist/esm/active-orders-widget.js +382 -0
  5. package/dist/esm/active-orders-widget.js.map +1 -0
  6. package/dist/esm/control-types.js +23 -0
  7. package/dist/esm/control-types.js.map +1 -0
  8. package/dist/esm/dom.js +55 -0
  9. package/dist/esm/dom.js.map +1 -0
  10. package/dist/esm/formatters.js +69 -0
  11. package/dist/esm/formatters.js.map +1 -0
  12. package/dist/esm/index.js +12 -0
  13. package/dist/esm/index.js.map +1 -0
  14. package/dist/esm/positions-widget.js +258 -0
  15. package/dist/esm/positions-widget.js.map +1 -0
  16. package/dist/esm/trade-history-widget.js +197 -0
  17. package/dist/esm/trade-history-widget.js.map +1 -0
  18. package/dist/esm/trading-data.js +26 -0
  19. package/dist/esm/trading-data.js.map +1 -0
  20. package/dist/esm/trading-host.js +79 -0
  21. package/dist/esm/trading-host.js.map +1 -0
  22. package/dist/esm/watchlist-widget.js +481 -0
  23. package/dist/esm/watchlist-widget.js.map +1 -0
  24. package/dist/sstradingcontrols.js +1740 -0
  25. package/dist/sstradingcontrols.js.map +7 -0
  26. package/dist/types/active-orders-widget.d.ts +52 -0
  27. package/dist/types/active-orders-widget.d.ts.map +1 -0
  28. package/dist/types/control-types.d.ts +8 -0
  29. package/dist/types/control-types.d.ts.map +1 -0
  30. package/dist/types/dom.d.ts +6 -0
  31. package/dist/types/dom.d.ts.map +1 -0
  32. package/dist/types/formatters.d.ts +8 -0
  33. package/dist/types/formatters.d.ts.map +1 -0
  34. package/dist/types/index.d.ts +16 -0
  35. package/dist/types/index.d.ts.map +1 -0
  36. package/dist/types/positions-widget.d.ts +37 -0
  37. package/dist/types/positions-widget.d.ts.map +1 -0
  38. package/dist/types/trade-history-widget.d.ts +25 -0
  39. package/dist/types/trade-history-widget.d.ts.map +1 -0
  40. package/dist/types/trading-data.d.ts +55 -0
  41. package/dist/types/trading-data.d.ts.map +1 -0
  42. package/dist/types/trading-host.d.ts +62 -0
  43. package/dist/types/trading-host.d.ts.map +1 -0
  44. package/dist/types/watchlist-widget.d.ts +60 -0
  45. package/dist/types/watchlist-widget.d.ts.map +1 -0
  46. package/package.json +129 -0
  47. package/src/active-orders-widget.ts +411 -0
  48. package/src/control-types.ts +24 -0
  49. package/src/dom.ts +68 -0
  50. package/src/formatters.ts +72 -0
  51. package/src/index.ts +52 -0
  52. package/src/positions-widget.ts +305 -0
  53. package/src/trade-history-widget.ts +221 -0
  54. package/src/trading-data.ts +121 -0
  55. package/src/trading-host.ts +300 -0
  56. package/src/watchlist-widget.ts +513 -0
  57. package/styles/theme.css +86 -0
  58. package/styles/trading-controls.css +469 -0
  59. 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
+ }