@stocksharp/trading-controls 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -16
- package/dist/esm/black-scholes.js +147 -0
- package/dist/esm/black-scholes.js.map +1 -0
- package/dist/esm/control-types.js +4 -0
- package/dist/esm/control-types.js.map +1 -1
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/log-monitor-widget.js +265 -0
- package/dist/esm/log-monitor-widget.js.map +1 -0
- package/dist/esm/log-tree.js +96 -0
- package/dist/esm/log-tree.js.map +1 -0
- package/dist/esm/option-desk-widget.js +322 -0
- package/dist/esm/option-desk-widget.js.map +1 -0
- package/dist/esm/pnl-curve.js +129 -0
- package/dist/esm/pnl-curve.js.map +1 -0
- package/dist/esm/statistics-widget.js +194 -0
- package/dist/esm/statistics-widget.js.map +1 -0
- package/dist/esm/strategies-widget.js +348 -0
- package/dist/esm/strategies-widget.js.map +1 -0
- package/dist/sstradingcontrols.js +1307 -47
- package/dist/sstradingcontrols.js.map +4 -4
- package/dist/types/black-scholes.d.ts +28 -0
- package/dist/types/black-scholes.d.ts.map +1 -0
- package/dist/types/control-types.d.ts +4 -0
- package/dist/types/control-types.d.ts.map +1 -1
- package/dist/types/index.d.ts +16 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/log-monitor-widget.d.ts +42 -0
- package/dist/types/log-monitor-widget.d.ts.map +1 -0
- package/dist/types/log-tree.d.ts +34 -0
- package/dist/types/log-tree.d.ts.map +1 -0
- package/dist/types/option-desk-widget.d.ts +68 -0
- package/dist/types/option-desk-widget.d.ts.map +1 -0
- package/dist/types/pnl-curve.d.ts +43 -0
- package/dist/types/pnl-curve.d.ts.map +1 -0
- package/dist/types/statistics-widget.d.ts +29 -0
- package/dist/types/statistics-widget.d.ts.map +1 -0
- package/dist/types/strategies-widget.d.ts +63 -0
- package/dist/types/strategies-widget.d.ts.map +1 -0
- package/dist/types/trading-data.d.ts +9 -0
- package/dist/types/trading-data.d.ts.map +1 -1
- package/package.json +27 -2
- package/screenshots/log-monitor.png +0 -0
- package/screenshots/option-desk.png +0 -0
- package/screenshots/panels.jpg +0 -0
- package/screenshots/statistics.png +0 -0
- package/screenshots/strategies.png +0 -0
- package/src/black-scholes.ts +199 -0
- package/src/control-types.ts +4 -0
- package/src/index.ts +41 -0
- package/src/log-monitor-widget.ts +312 -0
- package/src/log-tree.ts +131 -0
- package/src/option-desk-widget.ts +422 -0
- package/src/pnl-curve.ts +204 -0
- package/src/statistics-widget.ts +226 -0
- package/src/strategies-widget.ts +435 -0
- package/src/trading-data.ts +22 -0
- package/styles/trading-controls.css +356 -0
- package/translation-keys.json +70 -1
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@stocksharp/trading-controls)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
**StockSharp JS Trading Controls** are the browser panels a trading screen is
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
**StockSharp JS Trading Controls** are the browser panels a trading screen is made
|
|
8
|
+
of. Eleven of them: the **active orders**, **positions** and **trade history**
|
|
9
|
+
blotters, a **watchlist** with live quotes and category tabs, an **order entry**
|
|
10
|
+
pad, a **trade feed**, an **order book** ladder, a **statistics** table, a
|
|
11
|
+
**strategies** dashboard, a **log monitor**, and an **option desk**.
|
|
11
12
|
|
|
12
13
|
Each control builds its own DOM, renders its own table through
|
|
13
14
|
[`@stocksharp/grids`](https://www.npmjs.com/package/@stocksharp/grids), and reaches
|
|
@@ -86,15 +87,18 @@ clicking something that does nothing. So a missing member throws at construction
|
|
|
86
87
|
naming the path — `WatchlistWidget: host.trading.api.getExecutions is required` —
|
|
87
88
|
nested members included.
|
|
88
89
|
|
|
89
|
-
**
|
|
90
|
-
|
|
91
|
-
`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
**Every member is now reached by some shipped control.** When four of them were
|
|
91
|
+
here the port was already this wide, and the seven members nothing yet called —
|
|
92
|
+
`spawn`, `persistState`, `saveLayout`, `log`, `trading.pickInstrument`,
|
|
93
|
+
`marketData.resubscribe`, `marketData.getOrders` — were required anyway, because
|
|
94
|
+
the controls still on the terminal's side of the boundary were written against
|
|
95
|
+
this same interface and called them. Those controls have since moved in and do
|
|
96
|
+
call them, which is the argument for not having narrowed the port and widened it
|
|
97
|
+
again seven times.
|
|
98
|
+
|
|
99
|
+
A host adopting a subset still has to answer for all of it. Stubs are a correct
|
|
100
|
+
answer where a capability genuinely does not exist on that page: a no-op `spawn`,
|
|
101
|
+
a `log` that forwards to the console.
|
|
98
102
|
|
|
99
103
|
Two pairs are deliberately separate rather than merged:
|
|
100
104
|
|
|
@@ -121,7 +125,7 @@ Two pairs are deliberately separate rather than merged:
|
|
|
121
125
|
reads the port names (`getExecutions`, `searchInstruments`), which the host
|
|
122
126
|
implements.
|
|
123
127
|
|
|
124
|
-
### The
|
|
128
|
+
### The 222 keys a host has to answer
|
|
125
129
|
|
|
126
130
|
`t()` cannot fail. A key the host does not know is rendered to the user as
|
|
127
131
|
itself, so `NoActiveOrders` appears in the empty blotter and `ClosePanel`
|
|
@@ -130,11 +134,20 @@ error. The complete list ships with the package:
|
|
|
130
134
|
|
|
131
135
|
```ts
|
|
132
136
|
import keys from '@stocksharp/trading-controls/translation-keys.json';
|
|
133
|
-
// { count:
|
|
137
|
+
// { count: 222, keys: ['Actions', 'ActiveOrders', …] }
|
|
134
138
|
```
|
|
135
139
|
|
|
136
140
|
It is **generated from the sources** (`npm run i18n:update`) and re-checked by
|
|
137
|
-
`npm test`, so it cannot drift from what the controls actually ask for.
|
|
141
|
+
`npm test`, so it cannot drift from what the controls actually ask for. Because it
|
|
142
|
+
is generated by scanning for `t('literal')`, no control may assemble a key from a
|
|
143
|
+
value: a computed key would be invisible to the scan, absent from the list, and
|
|
144
|
+
would reach a user as itself. A test renders each control over data that takes
|
|
145
|
+
every branch of its wording and asserts that everything it asked for is on the
|
|
146
|
+
list.
|
|
147
|
+
|
|
148
|
+
One set of keys is deliberately not: the trading modes a host hands
|
|
149
|
+
`StrategiesWidget`. The host says what a run may be put in and therefore words
|
|
150
|
+
them, so those strings are its vocabulary and answering for them is its job.
|
|
138
151
|
|
|
139
152
|
The keys are not derivable, which is why the list is shipped rather than
|
|
140
153
|
described. Most are resource identifiers (`ClosePanel`, `ExportToExcel`,
|
|
@@ -304,6 +317,72 @@ is class names: the volume bar's share of its side and the heat behind a level
|
|
|
304
317
|
reach the stylesheet as measured custom properties (`--t-ob-bar`, `--t-ob-heat`,
|
|
305
318
|
`--t-ob-sent`), so no width and no colour is decided in TypeScript.
|
|
306
319
|
|
|
320
|
+
### `StatisticsWidget`
|
|
321
|
+
|
|
322
|
+
What a run made, one parameter per row, grouped by category and left in the order
|
|
323
|
+
the host sent them — the registry's order, which puts profit before trades before
|
|
324
|
+
positions before orders, and is not alphabetical. The panel measures nothing: a
|
|
325
|
+
statistic is produced by whatever ran the strategy, so the whole set arrives at
|
|
326
|
+
once through `update` and a row that stopped being sent has stopped existing.
|
|
327
|
+
There are no deps beyond the host, because there is nothing on the table to act
|
|
328
|
+
on. A value is a number, a moment or nothing at all, and nothing is a blank cell
|
|
329
|
+
rather than a zero: a figure that has not been measured yet is not a measured
|
|
330
|
+
zero.
|
|
331
|
+
|
|
332
|
+

|
|
333
|
+
|
|
334
|
+
### `StrategiesWidget`
|
|
335
|
+
|
|
336
|
+
A row per run: its state, whether it is online, the mode it trades in, its
|
|
337
|
+
instrument, position, order and trade counts, what it has made, and a sparkline of
|
|
338
|
+
how it got there. Every action is a dep and a row only offers what the host can
|
|
339
|
+
carry out — start appears on a stopped run and stop on a started one, the
|
|
340
|
+
trading-mode cell is a plain caption unless `setTradingMode` was supplied. The
|
|
341
|
+
modes themselves come from the host and are worded through `t()`, which makes them
|
|
342
|
+
the one set of keys in this package that the host answers for rather than the
|
|
343
|
+
shipped list.
|
|
344
|
+
|
|
345
|
+
The sparkline is a canvas sized in device pixels and drawn from
|
|
346
|
+
`presentation.canvasPalette()`, so it is as sharp as the text beside it and green
|
|
347
|
+
and red mean there what they mean everywhere else on the page.
|
|
348
|
+
|
|
349
|
+

|
|
350
|
+
|
|
351
|
+
### `LogMonitorWidget`
|
|
352
|
+
|
|
353
|
+
A source tree beside the messages, which is how a log with more than one writer is
|
|
354
|
+
read: pick a node and the table shows that source and everything under it. Sources
|
|
355
|
+
arrive whole through `setSources` and messages accumulate through `append`, capped
|
|
356
|
+
(`maxMessages`, five thousand by default) so a session left running overnight
|
|
357
|
+
cannot grow without bound. The five levels toggle independently and the filter is
|
|
358
|
+
a substring over the message and its source together.
|
|
359
|
+
|
|
360
|
+
A message names its source by id; what that id is *called* comes from the tree, so
|
|
361
|
+
renaming a source — or switching the page's language — re-captions the messages
|
|
362
|
+
that were already in the table.
|
|
363
|
+
|
|
364
|
+

|
|
365
|
+
|
|
366
|
+
### `OptionDeskWidget`
|
|
367
|
+
|
|
368
|
+
One expiry of a chain, the call side mirrored against the put side around the
|
|
369
|
+
strike. Volume and open interest are scaled per side, because calls and puts trade
|
|
370
|
+
in different sizes and comparing one against the busiest of the other says
|
|
371
|
+
nothing; volatility is scaled across both at once, because a skew is exactly the
|
|
372
|
+
comparison between them.
|
|
373
|
+
|
|
374
|
+
Greeks arrive one of two ways and the desk does not care which: a host that prices
|
|
375
|
+
its own sends them on the contract, and a host that has quotes and an expiry sends
|
|
376
|
+
the volatility instead and the desk prices delta through rho itself, from the
|
|
377
|
+
Black-Scholes that ships with the package (`premium`, `greeks`,
|
|
378
|
+
`impliedVolatility` — exported, because a host that wants the arithmetic without
|
|
379
|
+
the table should not have to reimplement it). The number of decimal places in a
|
|
380
|
+
greek column comes from the numbers in it: gamma on an underlying at sixty
|
|
381
|
+
thousand is about 0.00003, and the four places that suit a delta would show every
|
|
382
|
+
strike as nothing.
|
|
383
|
+
|
|
384
|
+

|
|
385
|
+
|
|
307
386
|
## Exported sheets say what the screen says
|
|
308
387
|
|
|
309
388
|
Every blotter exports to `.xlsx` through the grid, and a column states both
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Option pricing and the greeks, as pure functions.
|
|
2
|
+
//
|
|
3
|
+
// The desktop desk gets these from StockSharp.Algo.Derivatives, which a browser cannot call. A
|
|
4
|
+
// host could compute them and send them down, and one that already does should - the desk takes
|
|
5
|
+
// them either way. But a host that has bid, ask and an expiry has everything the arithmetic
|
|
6
|
+
// needs, and making it stand up a pricing service before it can draw a chain is a poor trade.
|
|
7
|
+
//
|
|
8
|
+
// So the maths ships, and it ships here, apart from the control: a wrong greek is not visible
|
|
9
|
+
// on screen the way a wrong column is, and the only defence is checking the numbers against
|
|
10
|
+
// values that are known.
|
|
11
|
+
//
|
|
12
|
+
// Conventions follow the desk that reads them, not the textbook: vega is per one point of
|
|
13
|
+
// volatility, theta is per calendar day, and rho is per one point of rate - the units a trader
|
|
14
|
+
// works in, rather than per 1.0 of each, which nobody quotes.
|
|
15
|
+
export const OptionTypes = {
|
|
16
|
+
Call: 'call',
|
|
17
|
+
Put: 'put',
|
|
18
|
+
};
|
|
19
|
+
const SQRT_2PI = Math.sqrt(2 * Math.PI);
|
|
20
|
+
const DAYS_IN_YEAR = 365;
|
|
21
|
+
/// The standard normal CDF.
|
|
22
|
+
///
|
|
23
|
+
/// Hart's rational approximation, which is what pricing libraries use and what a desk's numbers
|
|
24
|
+
/// are expected to agree with: accurate to about 1e-15 across the range, and exactly a half at
|
|
25
|
+
/// the mean rather than nearly so. The cheaper Abramowitz-and-Stegun fit is good to 1e-7, which
|
|
26
|
+
/// sounds like enough until a greek that should be symmetric is not.
|
|
27
|
+
export function normalCdf(x) {
|
|
28
|
+
const z = Math.abs(x);
|
|
29
|
+
// Beyond this the tail is smaller than double precision can carry anyway.
|
|
30
|
+
if (z > 37)
|
|
31
|
+
return x > 0 ? 1 : 0;
|
|
32
|
+
const e = Math.exp(-(z * z) / 2);
|
|
33
|
+
let tail;
|
|
34
|
+
if (z < 7.07106781186547) {
|
|
35
|
+
let b = 3.52624965998911e-02 * z + 0.700383064443688;
|
|
36
|
+
b = b * z + 6.37396220353165;
|
|
37
|
+
b = b * z + 33.912866078383;
|
|
38
|
+
b = b * z + 112.079291497871;
|
|
39
|
+
b = b * z + 221.213596169931;
|
|
40
|
+
b = b * z + 220.206867912376;
|
|
41
|
+
let d = 8.83883476483184e-02 * z + 1.75566716318264;
|
|
42
|
+
d = d * z + 16.064177579207;
|
|
43
|
+
d = d * z + 86.7807322029461;
|
|
44
|
+
d = d * z + 296.564248779674;
|
|
45
|
+
d = d * z + 637.333633378831;
|
|
46
|
+
d = d * z + 793.826512519948;
|
|
47
|
+
d = d * z + 440.413735824752;
|
|
48
|
+
tail = (e * b) / d;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// A continued fraction, which is what stays accurate once the polynomial ratio above
|
|
52
|
+
// starts losing digits to cancellation.
|
|
53
|
+
let b = z + 0.65;
|
|
54
|
+
b = z + 4 / b;
|
|
55
|
+
b = z + 3 / b;
|
|
56
|
+
b = z + 2 / b;
|
|
57
|
+
b = z + 1 / b;
|
|
58
|
+
tail = e / (b * 2.506628274631);
|
|
59
|
+
}
|
|
60
|
+
return x > 0 ? 1 - tail : tail;
|
|
61
|
+
}
|
|
62
|
+
/// The standard normal density.
|
|
63
|
+
export function normalPdf(x) {
|
|
64
|
+
return Math.exp(-0.5 * x * x) / SQRT_2PI;
|
|
65
|
+
}
|
|
66
|
+
/// d1, or zero when there is no time or no volatility for the division to mean anything.
|
|
67
|
+
export function d1(inputs) {
|
|
68
|
+
const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
|
|
69
|
+
const spread = deviation * Math.sqrt(timeToExpiry);
|
|
70
|
+
if (spread === 0 || assetPrice <= 0 || strike <= 0)
|
|
71
|
+
return 0;
|
|
72
|
+
return (Math.log(assetPrice / strike) + (riskFree - dividend + (deviation * deviation) / 2) * timeToExpiry) / spread;
|
|
73
|
+
}
|
|
74
|
+
/// d2, which is d1 less one standard deviation of the remaining time.
|
|
75
|
+
export function d2(inputs) {
|
|
76
|
+
const spread = inputs.deviation * Math.sqrt(inputs.timeToExpiry);
|
|
77
|
+
return spread === 0 ? 0 : d1(inputs) - spread;
|
|
78
|
+
}
|
|
79
|
+
/// What the option is worth.
|
|
80
|
+
///
|
|
81
|
+
/// At expiry, or with no volatility, that is its intrinsic value - the formula degenerates to
|
|
82
|
+
/// exactly that, and saying so explicitly keeps a zero denominator out of the general case.
|
|
83
|
+
export function premium(type, inputs) {
|
|
84
|
+
const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
|
|
85
|
+
if (timeToExpiry <= 0 || deviation <= 0)
|
|
86
|
+
return Math.max(0, type === OptionTypes.Call ? assetPrice - strike : strike - assetPrice);
|
|
87
|
+
const a = d1(inputs);
|
|
88
|
+
const b = d2(inputs);
|
|
89
|
+
const carried = assetPrice * Math.exp(-dividend * timeToExpiry);
|
|
90
|
+
const discounted = strike * Math.exp(-riskFree * timeToExpiry);
|
|
91
|
+
return type === OptionTypes.Call
|
|
92
|
+
? carried * normalCdf(a) - discounted * normalCdf(b)
|
|
93
|
+
: discounted * normalCdf(-b) - carried * normalCdf(-a);
|
|
94
|
+
}
|
|
95
|
+
/// Every greek at once: they share d1, and computing them together is both cheaper and the only
|
|
96
|
+
/// way they are guaranteed to describe the same moment.
|
|
97
|
+
export function greeks(type, inputs) {
|
|
98
|
+
const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
|
|
99
|
+
const sign = type === OptionTypes.Call ? 1 : -1;
|
|
100
|
+
if (timeToExpiry <= 0 || deviation <= 0 || assetPrice <= 0) {
|
|
101
|
+
// An expired option still has a delta - it is one or nothing, depending on which side of
|
|
102
|
+
// the strike it finished. Everything else has stopped moving.
|
|
103
|
+
const inTheMoney = type === OptionTypes.Call ? assetPrice > strike : assetPrice < strike;
|
|
104
|
+
return { delta: inTheMoney ? sign : 0, gamma: 0, vega: 0, theta: 0, rho: 0 };
|
|
105
|
+
}
|
|
106
|
+
const a = d1(inputs);
|
|
107
|
+
const b = d2(inputs);
|
|
108
|
+
const sqrtT = Math.sqrt(timeToExpiry);
|
|
109
|
+
const density = normalPdf(a);
|
|
110
|
+
const carry = Math.exp(-dividend * timeToExpiry);
|
|
111
|
+
const discount = Math.exp(-riskFree * timeToExpiry);
|
|
112
|
+
const delta = sign * carry * normalCdf(sign * a);
|
|
113
|
+
const gamma = (carry * density) / (assetPrice * deviation * sqrtT);
|
|
114
|
+
// Scaled the way a desk quotes them: vega per one volatility point, rho per one rate point,
|
|
115
|
+
// theta per calendar day.
|
|
116
|
+
const vega = assetPrice * carry * density * sqrtT * 0.01;
|
|
117
|
+
const rho = sign * strike * timeToExpiry * discount * normalCdf(sign * b) * 0.01;
|
|
118
|
+
const theta = (-(assetPrice * carry * density * deviation) / (2 * sqrtT)
|
|
119
|
+
- sign * riskFree * strike * discount * normalCdf(sign * b)
|
|
120
|
+
+ sign * dividend * assetPrice * carry * normalCdf(sign * a)) / DAYS_IN_YEAR;
|
|
121
|
+
return { delta, gamma, vega, theta, rho };
|
|
122
|
+
}
|
|
123
|
+
/// The volatility that would produce this price, or null when none would.
|
|
124
|
+
///
|
|
125
|
+
/// Bisection rather than Newton: vega collapses far from the money and deep in time, and a
|
|
126
|
+
/// Newton step divided by a vanishing vega walks off to nonsense. Halving cannot, and forty
|
|
127
|
+
/// steps over a range this wide is finer than any quote.
|
|
128
|
+
export function impliedVolatility(type, inputs, price) {
|
|
129
|
+
if (!(price > 0) || inputs.timeToExpiry <= 0)
|
|
130
|
+
return null;
|
|
131
|
+
const at = (deviation) => premium(type, { ...inputs, deviation });
|
|
132
|
+
let low = 1e-6;
|
|
133
|
+
let high = 5;
|
|
134
|
+
// A price outside what any volatility in that range can produce has no answer, and
|
|
135
|
+
// returning the nearest bound would be a number that means nothing.
|
|
136
|
+
if (price < at(low) || price > at(high))
|
|
137
|
+
return null;
|
|
138
|
+
for (let step = 0; step < 60; step++) {
|
|
139
|
+
const mid = (low + high) / 2;
|
|
140
|
+
if (at(mid) < price)
|
|
141
|
+
low = mid;
|
|
142
|
+
else
|
|
143
|
+
high = mid;
|
|
144
|
+
}
|
|
145
|
+
return (low + high) / 2;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=black-scholes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"black-scholes.js","sourceRoot":"","sources":["../../src/black-scholes.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,5 +22,9 @@ export const ControlTypes = {
|
|
|
22
22
|
OrderBook: 'orderbook',
|
|
23
23
|
TradeFeed: 'tradefeed',
|
|
24
24
|
OrderEntry: 'orderEntry',
|
|
25
|
+
Statistics: 'statistics',
|
|
26
|
+
LogMonitor: 'logMonitor',
|
|
27
|
+
Strategies: 'strategies',
|
|
28
|
+
OptionDesk: 'optionDesk',
|
|
25
29
|
};
|
|
26
30
|
//# sourceMappingURL=control-types.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-types.js","sourceRoot":"","sources":["../../src/control-types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,iBAAiB;AACjB,EAAE;AACF,gFAAgF;AAChF,yEAAyE;AACzE,4EAA4E;AAC5E,8EAA8E;AAC9E,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,2BAA2B;AAC3B,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,SAAS;AACT,MAAM,CAAC,MAAM,YAAY,GAAG;IACxB,SAAS,EAAE,WAAW;IACtB,YAAY,EAAE,cAAc;IAC5B,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,UAAU,EAAE,YAAY;CAClB,CAAC"}
|
|
1
|
+
{"version":3,"file":"control-types.js","sourceRoot":"","sources":["../../src/control-types.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,iBAAiB;AACjB,EAAE;AACF,gFAAgF;AAChF,yEAAyE;AACzE,4EAA4E;AAC5E,8EAA8E;AAC9E,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,2BAA2B;AAC3B,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,SAAS;AACT,MAAM,CAAC,MAAM,YAAY,GAAG;IACxB,SAAS,EAAE,WAAW;IACtB,YAAY,EAAE,cAAc;IAC5B,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;IACxB,UAAU,EAAE,YAAY;CAClB,CAAC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -11,10 +11,28 @@ export { TradeHistoryWidget } from './trade-history-widget.js';
|
|
|
11
11
|
export { WatchlistWidget } from './watchlist-widget.js';
|
|
12
12
|
export { OrderEntrySides, OrderEntryTypes, OrderEntryWidget } from './order-entry-widget.js';
|
|
13
13
|
export { OrderBookWidget } from './orderbook-widget.js';
|
|
14
|
+
// The cumulative P&L curve, shared by everything that draws one: a strategy row's
|
|
15
|
+
// sparkline and a statistics card are the same curve at two sizes. A backtest's own
|
|
16
|
+
// equity is not here - that one shares the price axis and belongs to the chart engine.
|
|
17
|
+
export { compressPnl, drawPnlCurve, pnlCurve } from './pnl-curve.js';
|
|
18
|
+
export { OptionDeskWidget, greekPlaces, greekScales, scaleChain, sideGreeks } from './option-desk-widget.js';
|
|
19
|
+
// Option pricing, for a host that has quotes and an expiry but no pricing service. A host
|
|
20
|
+
// that computes its own greeks sends them instead and none of this is reached.
|
|
21
|
+
export { OptionTypes, d1, d2, greeks, impliedVolatility, normalCdf, normalPdf, premium } from './black-scholes.js';
|
|
22
|
+
export { StrategiesWidget, StrategyStates } from './strategies-widget.js';
|
|
23
|
+
export { LogMonitorWidget } from './log-monitor-widget.js';
|
|
24
|
+
// The log's own shapes: how sources nest, and which messages a view of them keeps. A host
|
|
25
|
+
// rendering its own view of the same feed reads these rather than reinventing the filter.
|
|
26
|
+
export { LogLevels, buildLogTree, keepLog, subtreeOf } from './log-tree.js';
|
|
27
|
+
export { StatisticsWidget, formatStatistic } from './statistics-widget.js';
|
|
14
28
|
export { TradeFeedWidget } from './tradefeed-widget.js';
|
|
15
29
|
// The tape's own shapes: one print as the chart reads it, and the bubble a
|
|
16
30
|
// print (or a slice of them) is drawn as. A host that renders its own view of
|
|
17
31
|
// the same feed reads these rather than reinventing the compaction.
|
|
18
32
|
export { aggregateBubbles } from './tradefeed-aggregator.js';
|
|
19
33
|
export { layoutBubbles } from './tradefeed-bubbles.js';
|
|
34
|
+
// The grid's own menu, worded by the host. Exported because a host that builds a table of its
|
|
35
|
+
// own - a run's log beside the blotters, say - needs the same menu in the same words, and the
|
|
36
|
+
// alternative is every consumer restating thirty labels the package already knows.
|
|
37
|
+
export { makeGridMenu } from './grid-menu.js';
|
|
20
38
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AA2BvF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIlD,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEnG,8EAA8E;AAC9E,8EAA8E;AAC9E,eAAe;AACf,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAI5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAI/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAM7F,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAUxD,kFAAkF;AAClF,oFAAoF;AACpF,uFAAuF;AACvF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAIrE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAI7G,0FAA0F;AAC1F,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAInH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAI1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAI3D,0FAA0F;AAC1F,0FAA0F;AAC1F,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI5E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAI3E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,2EAA2E;AAC3E,8EAA8E;AAC9E,oEAAoE;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAI7D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAIvD,8FAA8F;AAC9F,8FAA8F;AAC9F,mFAAmF;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Log monitor — multi-instance.
|
|
2
|
+
//
|
|
3
|
+
// Two panes: the tree of everything that writes a log on the left, and on the right what the
|
|
4
|
+
// selected source and everything under it has written. The desktop version copies each message
|
|
5
|
+
// into every ancestor's list, so one line is held as many times as it has ancestors and its own
|
|
6
|
+
// counter reports copies rather than messages. Here a message is held once and carries the id
|
|
7
|
+
// of the source that wrote it; selecting a node filters by its subtree.
|
|
8
|
+
//
|
|
9
|
+
// A live log has no end, so this one has a cap. The desktop's grows for the lifetime of the
|
|
10
|
+
// window - nothing trims it - which a browser tab cannot afford.
|
|
11
|
+
import { makeElement, makeIconButton, makePanelId, makePanelRoot } from './dom.js';
|
|
12
|
+
import { ControlTypes } from './control-types.js';
|
|
13
|
+
import { makeGridMenu } from './grid-menu.js';
|
|
14
|
+
import { assertHost } from './trading-host.js';
|
|
15
|
+
import { LogLevels, buildLogTree, keepLog, subtreeOf, } from './log-tree.js';
|
|
16
|
+
import { DataGrid } from '@stocksharp/grids/data-grid';
|
|
17
|
+
const DEFAULT_MAX = 5000;
|
|
18
|
+
/// The letter a level shows in the narrow column, and the class that colours the row.
|
|
19
|
+
///
|
|
20
|
+
/// Info gets a letter here. The desktop leaves its cell blank - there is simply no trigger for
|
|
21
|
+
/// it among the four that exist - so the commonest level is the one with no marking at all,
|
|
22
|
+
/// which is an omission rather than a decision.
|
|
23
|
+
const LEVEL_LETTER = {
|
|
24
|
+
[LogLevels.Error]: 'E',
|
|
25
|
+
[LogLevels.Warning]: 'W',
|
|
26
|
+
[LogLevels.Info]: 'I',
|
|
27
|
+
[LogLevels.Debug]: 'D',
|
|
28
|
+
[LogLevels.Verbose]: 'V',
|
|
29
|
+
};
|
|
30
|
+
const ALL_LEVELS = [LogLevels.Error, LogLevels.Warning, LogLevels.Info, LogLevels.Debug, LogLevels.Verbose];
|
|
31
|
+
export class LogMonitorWidget {
|
|
32
|
+
static create(hostEl, state, deps) {
|
|
33
|
+
const host = assertHost(deps?.host, 'LogMonitorWidget');
|
|
34
|
+
const root = LogMonitorWidget._buildRoot(host);
|
|
35
|
+
root.id = makePanelId(LogMonitorWidget.TYPE);
|
|
36
|
+
hostEl.appendChild(root);
|
|
37
|
+
return new LogMonitorWidget(root, state || {}, deps);
|
|
38
|
+
}
|
|
39
|
+
// The panel's markup: a source tree, a toolbar of level toggles and a text box, and the
|
|
40
|
+
// message table. The level toggles are buttons rather than checkboxes because they are read
|
|
41
|
+
// as a row of states, and each carries its letter so the toolbar and the column agree.
|
|
42
|
+
static _buildRoot(host) {
|
|
43
|
+
const title = host.t('LogMonitor');
|
|
44
|
+
return makePanelRoot('log-monitor-panel', title, [
|
|
45
|
+
makeElement('div', 'panel-header', {}, [
|
|
46
|
+
makeElement('span', '', {}, [title]),
|
|
47
|
+
makeIconButton('bt-icon-btn bt-icon-cancel panel-close-btn', host.t('ClosePanel'), 'bi-x', { type: 'button' }),
|
|
48
|
+
]),
|
|
49
|
+
makeElement('div', 'panel-body log-monitor-body', {}, [
|
|
50
|
+
makeElement('div', 'log-sources', { role: 'tree', 'aria-label': host.t('LogSources') }, []),
|
|
51
|
+
makeElement('div', 'log-messages', {}, [
|
|
52
|
+
makeElement('div', 'log-toolbar', { role: 'toolbar', 'aria-label': host.t('LogMonitorActions') }, [
|
|
53
|
+
...ALL_LEVELS.map(level => makeElement('button', `log-level-toggle log-level-${level} is-on`, {
|
|
54
|
+
type: 'button',
|
|
55
|
+
'data-level': level,
|
|
56
|
+
'aria-pressed': 'true',
|
|
57
|
+
title: levelTitle(host, level),
|
|
58
|
+
}, [LEVEL_LETTER[level]])),
|
|
59
|
+
makeElement('input', 'form-control form-control-sm log-filter', {
|
|
60
|
+
type: 'search',
|
|
61
|
+
placeholder: host.t('Filter'),
|
|
62
|
+
'aria-label': host.t('Filter'),
|
|
63
|
+
}, []),
|
|
64
|
+
makeIconButton('bt-icon-btn log-clear-btn', host.t('ClearItems'), 'bi-eraser', {}),
|
|
65
|
+
makeIconButton('bt-icon-btn panel-export-btn', host.t('ExportToExcel'), 'bi-file-earmark-spreadsheet', {}),
|
|
66
|
+
]),
|
|
67
|
+
// The table sits in a box of its own rather than being a flex child: a table
|
|
68
|
+
// told to fill a column stretches its rows to do it, so a log holding one
|
|
69
|
+
// line drew that line a panel tall - and with the toolbar and the table as
|
|
70
|
+
// the only two children there was nowhere for a long log to scroll.
|
|
71
|
+
makeElement('div', 'log-table-scroll', {}, [
|
|
72
|
+
makeElement('table', 'terminal-table log-table', { role: 'table', 'aria-label': host.t('LogMessages') }, [
|
|
73
|
+
makeElement('thead', '', {}, []),
|
|
74
|
+
makeElement('tbody', 'log-body', {}, []),
|
|
75
|
+
]),
|
|
76
|
+
]),
|
|
77
|
+
]),
|
|
78
|
+
]),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
constructor(rootEl, _state, deps) {
|
|
82
|
+
this._host = assertHost(deps?.host, 'LogMonitorWidget');
|
|
83
|
+
this._max = deps?.maxMessages && deps.maxMessages > 0 ? deps.maxMessages : DEFAULT_MAX;
|
|
84
|
+
this.rootEl = rootEl;
|
|
85
|
+
this.el = this.rootEl.querySelector('.log-body');
|
|
86
|
+
this._treeEl = this.rootEl.querySelector('.log-sources');
|
|
87
|
+
this._filterEl = this.rootEl.querySelector('.log-filter');
|
|
88
|
+
this._closeBtn = this.rootEl.querySelector('.panel-close-btn');
|
|
89
|
+
this._clearBtn = this.rootEl.querySelector('.log-clear-btn');
|
|
90
|
+
this._exportBtn = this.rootEl.querySelector('.panel-export-btn');
|
|
91
|
+
this._sources = [];
|
|
92
|
+
this._messages = [];
|
|
93
|
+
this._levels = new Set(ALL_LEVELS);
|
|
94
|
+
this._text = '';
|
|
95
|
+
this._selected = null;
|
|
96
|
+
this._closeBtn?.addEventListener('click', (e) => { e.preventDefault(); this._host.close(); });
|
|
97
|
+
this._clearBtn?.addEventListener('click', (e) => { e.preventDefault(); this.clear(); });
|
|
98
|
+
this._exportBtn?.addEventListener('click', (e) => { e.preventDefault(); this._export(); });
|
|
99
|
+
for (const toggle of Array.from(this.rootEl.querySelectorAll('.log-level-toggle'))) {
|
|
100
|
+
toggle.addEventListener('click', (e) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
const level = toggle.getAttribute('data-level') ?? '';
|
|
103
|
+
if (this._levels.has(level))
|
|
104
|
+
this._levels.delete(level);
|
|
105
|
+
else
|
|
106
|
+
this._levels.add(level);
|
|
107
|
+
toggle.classList.toggle('is-on', this._levels.has(level));
|
|
108
|
+
toggle.setAttribute('aria-pressed', this._levels.has(level) ? 'true' : 'false');
|
|
109
|
+
this._render();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
this._filterEl?.addEventListener('input', () => {
|
|
113
|
+
this._text = this._filterEl?.value ?? '';
|
|
114
|
+
this._render();
|
|
115
|
+
});
|
|
116
|
+
const head = this.rootEl.querySelector('.log-table thead');
|
|
117
|
+
this._grid = head && this.el
|
|
118
|
+
? new DataGrid({
|
|
119
|
+
head: head,
|
|
120
|
+
body: this.el,
|
|
121
|
+
columns: this._columns(),
|
|
122
|
+
// The order it happened in. A log read out of order is not a log.
|
|
123
|
+
defaultSort: { col: 'time', dir: 'asc' },
|
|
124
|
+
rowKey: (m) => String(m.id),
|
|
125
|
+
emptyText: this._host.t('NoLogMessages'),
|
|
126
|
+
rowClass: (m) => `log-row log-row-${m.level}`,
|
|
127
|
+
contextMenu: makeGridMenu(this._host),
|
|
128
|
+
selection: 'multi',
|
|
129
|
+
})
|
|
130
|
+
: null;
|
|
131
|
+
this._host.register(this);
|
|
132
|
+
}
|
|
133
|
+
dispose() {
|
|
134
|
+
this._grid?.destroy();
|
|
135
|
+
this._host.unregister(this);
|
|
136
|
+
try {
|
|
137
|
+
this.rootEl.remove();
|
|
138
|
+
}
|
|
139
|
+
catch { /* already detached */ }
|
|
140
|
+
}
|
|
141
|
+
/// The sources that exist. Sent whole: a source that has gone is gone from the tree, and
|
|
142
|
+
/// the selection falls back to everything rather than to a node nobody can see.
|
|
143
|
+
setSources(sources) {
|
|
144
|
+
this._sources = sources || [];
|
|
145
|
+
if (this._selected !== null && !this._sources.some(s => s.id === this._selected))
|
|
146
|
+
this._selected = null;
|
|
147
|
+
this._renderTree();
|
|
148
|
+
this._render();
|
|
149
|
+
}
|
|
150
|
+
/// Add what has just been written. The oldest fall off the front once the cap is reached.
|
|
151
|
+
append(messages) {
|
|
152
|
+
if (!messages || messages.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
this._messages.push(...messages);
|
|
155
|
+
if (this._messages.length > this._max)
|
|
156
|
+
this._messages.splice(0, this._messages.length - this._max);
|
|
157
|
+
this._render();
|
|
158
|
+
}
|
|
159
|
+
/// Forget every message. The sources stay: they still exist, they have just gone quiet.
|
|
160
|
+
clear() {
|
|
161
|
+
this._messages = [];
|
|
162
|
+
this._render();
|
|
163
|
+
}
|
|
164
|
+
/// Show one source's subtree, or everything when given null.
|
|
165
|
+
select(sourceId) {
|
|
166
|
+
this._selected = sourceId;
|
|
167
|
+
this._renderTree();
|
|
168
|
+
this._render();
|
|
169
|
+
}
|
|
170
|
+
/// What is on screen, after every filter.
|
|
171
|
+
visible() {
|
|
172
|
+
const sources = subtreeOf(this._sources, this._selected);
|
|
173
|
+
return this._messages.filter(m => keepLog(m, { levels: this._levels, text: this._text, sources }));
|
|
174
|
+
}
|
|
175
|
+
_render() {
|
|
176
|
+
this._grid?.setRows(this.visible());
|
|
177
|
+
}
|
|
178
|
+
_renderTree() {
|
|
179
|
+
const tree = this._treeEl;
|
|
180
|
+
if (tree === null)
|
|
181
|
+
return;
|
|
182
|
+
tree.innerHTML = '';
|
|
183
|
+
tree.appendChild(this._treeRow({ id: '', name: this._host.t('AllSources'), depth: 0, children: [] }, null));
|
|
184
|
+
for (const root of buildLogTree(this._sources))
|
|
185
|
+
this._appendNode(tree, root);
|
|
186
|
+
}
|
|
187
|
+
_appendNode(into, node) {
|
|
188
|
+
into.appendChild(this._treeRow(node, node.id));
|
|
189
|
+
for (const child of node.children)
|
|
190
|
+
this._appendNode(into, child);
|
|
191
|
+
}
|
|
192
|
+
// Flat rows with an indent rather than nested lists: the tree is read, not restructured,
|
|
193
|
+
// and one list is what a keyboard walks through in the order the eye does.
|
|
194
|
+
_treeRow(node, id) {
|
|
195
|
+
const selected = this._selected === id;
|
|
196
|
+
const row = makeElement('button', `log-source${selected ? ' is-selected' : ''}`, {
|
|
197
|
+
type: 'button',
|
|
198
|
+
role: 'treeitem',
|
|
199
|
+
'aria-selected': selected ? 'true' : 'false',
|
|
200
|
+
'aria-level': String(node.depth + 1),
|
|
201
|
+
style: `padding-left: ${0.5 + node.depth * 0.85}rem`,
|
|
202
|
+
}, [node.name]);
|
|
203
|
+
row.addEventListener('click', (e) => { e.preventDefault(); this.select(id); });
|
|
204
|
+
return row;
|
|
205
|
+
}
|
|
206
|
+
_export() {
|
|
207
|
+
this._grid?.download('log', this._host.t('LogMonitor'));
|
|
208
|
+
}
|
|
209
|
+
_columns() {
|
|
210
|
+
const label = (key) => this._host.t(key);
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
key: 'source',
|
|
214
|
+
header: label('Source'),
|
|
215
|
+
exportable: true,
|
|
216
|
+
value: (m) => m.source ?? this._sourceName(m.sourceId),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
key: 'time',
|
|
220
|
+
header: label('Time'),
|
|
221
|
+
exportable: true,
|
|
222
|
+
value: (m) => m.time,
|
|
223
|
+
render: (m) => this._host.presentation.timeText(m.time),
|
|
224
|
+
exportValue: (m) => this._host.presentation.timeText(m.time),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
key: 'level',
|
|
228
|
+
header: label('Type'),
|
|
229
|
+
headerClass: 'log-level-col',
|
|
230
|
+
exportable: true,
|
|
231
|
+
cellClass: (m) => `log-level-cell log-level-${m.level}`,
|
|
232
|
+
value: (m) => m.level,
|
|
233
|
+
render: (m) => LEVEL_LETTER[m.level] ?? String(m.level ?? '').slice(0, 1).toUpperCase(),
|
|
234
|
+
// The sheet carries the level's name, not the letter the column has room for.
|
|
235
|
+
exportValue: (m) => m.level,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
key: 'message',
|
|
239
|
+
header: label('Message'),
|
|
240
|
+
exportable: true,
|
|
241
|
+
cellClass: () => 'log-message-cell',
|
|
242
|
+
value: (m) => m.message,
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
_sourceName(id) {
|
|
247
|
+
return this._sources.find(s => s.id === id)?.name ?? id;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
LogMonitorWidget.TYPE = ControlTypes.LogMonitor;
|
|
251
|
+
/// What a level's toggle is called.
|
|
252
|
+
///
|
|
253
|
+
/// Literal keys at the point they are asked for - see the note on the same shape in
|
|
254
|
+
/// strategies-widget: a key built from a value is invisible to the scan that generates
|
|
255
|
+
/// `translation-keys.json`, and reaches the user untranslated.
|
|
256
|
+
function levelTitle(host, level) {
|
|
257
|
+
switch (level) {
|
|
258
|
+
case LogLevels.Error: return host.t('Errors');
|
|
259
|
+
case LogLevels.Warning: return host.t('Warnings');
|
|
260
|
+
case LogLevels.Info: return host.t('Messages');
|
|
261
|
+
case LogLevels.Debug: return host.t('Debug');
|
|
262
|
+
default: return host.t('Verbose');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=log-monitor-widget.js.map
|