@stocksharp/trading-controls 1.1.1 → 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.
Files changed (77) hide show
  1. package/README.md +95 -16
  2. package/dist/esm/active-orders-widget.js +38 -25
  3. package/dist/esm/active-orders-widget.js.map +1 -1
  4. package/dist/esm/black-scholes.js +147 -0
  5. package/dist/esm/black-scholes.js.map +1 -0
  6. package/dist/esm/control-types.js +4 -0
  7. package/dist/esm/control-types.js.map +1 -1
  8. package/dist/esm/index.js +18 -0
  9. package/dist/esm/index.js.map +1 -1
  10. package/dist/esm/log-monitor-widget.js +265 -0
  11. package/dist/esm/log-monitor-widget.js.map +1 -0
  12. package/dist/esm/log-tree.js +96 -0
  13. package/dist/esm/log-tree.js.map +1 -0
  14. package/dist/esm/option-desk-widget.js +322 -0
  15. package/dist/esm/option-desk-widget.js.map +1 -0
  16. package/dist/esm/pnl-curve.js +129 -0
  17. package/dist/esm/pnl-curve.js.map +1 -0
  18. package/dist/esm/statistics-widget.js +194 -0
  19. package/dist/esm/statistics-widget.js.map +1 -0
  20. package/dist/esm/strategies-widget.js +348 -0
  21. package/dist/esm/strategies-widget.js.map +1 -0
  22. package/dist/esm/trade-history-widget.js +14 -6
  23. package/dist/esm/trade-history-widget.js.map +1 -1
  24. package/dist/esm/tradefeed-widget.js +3 -3
  25. package/dist/esm/tradefeed-widget.js.map +1 -1
  26. package/dist/esm/trading-host.js +1 -0
  27. package/dist/esm/trading-host.js.map +1 -1
  28. package/dist/sstradingcontrols.js +1358 -78
  29. package/dist/sstradingcontrols.js.map +4 -4
  30. package/dist/types/active-orders-widget.d.ts +11 -3
  31. package/dist/types/active-orders-widget.d.ts.map +1 -1
  32. package/dist/types/black-scholes.d.ts +28 -0
  33. package/dist/types/black-scholes.d.ts.map +1 -0
  34. package/dist/types/control-types.d.ts +4 -0
  35. package/dist/types/control-types.d.ts.map +1 -1
  36. package/dist/types/index.d.ts +16 -1
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/log-monitor-widget.d.ts +42 -0
  39. package/dist/types/log-monitor-widget.d.ts.map +1 -0
  40. package/dist/types/log-tree.d.ts +34 -0
  41. package/dist/types/log-tree.d.ts.map +1 -0
  42. package/dist/types/option-desk-widget.d.ts +68 -0
  43. package/dist/types/option-desk-widget.d.ts.map +1 -0
  44. package/dist/types/pnl-curve.d.ts +43 -0
  45. package/dist/types/pnl-curve.d.ts.map +1 -0
  46. package/dist/types/statistics-widget.d.ts +29 -0
  47. package/dist/types/statistics-widget.d.ts.map +1 -0
  48. package/dist/types/strategies-widget.d.ts +63 -0
  49. package/dist/types/strategies-widget.d.ts.map +1 -0
  50. package/dist/types/trade-history-widget.d.ts +3 -1
  51. package/dist/types/trade-history-widget.d.ts.map +1 -1
  52. package/dist/types/trading-data.d.ts +9 -0
  53. package/dist/types/trading-data.d.ts.map +1 -1
  54. package/dist/types/trading-host.d.ts +1 -0
  55. package/dist/types/trading-host.d.ts.map +1 -1
  56. package/package.json +27 -2
  57. package/screenshots/log-monitor.png +0 -0
  58. package/screenshots/option-desk.png +0 -0
  59. package/screenshots/panels.jpg +0 -0
  60. package/screenshots/statistics.png +0 -0
  61. package/screenshots/strategies.png +0 -0
  62. package/src/active-orders-widget.ts +49 -25
  63. package/src/black-scholes.ts +199 -0
  64. package/src/control-types.ts +4 -0
  65. package/src/index.ts +41 -0
  66. package/src/log-monitor-widget.ts +312 -0
  67. package/src/log-tree.ts +131 -0
  68. package/src/option-desk-widget.ts +422 -0
  69. package/src/pnl-curve.ts +204 -0
  70. package/src/statistics-widget.ts +226 -0
  71. package/src/strategies-widget.ts +435 -0
  72. package/src/trade-history-widget.ts +19 -6
  73. package/src/tradefeed-widget.ts +3 -3
  74. package/src/trading-data.ts +22 -0
  75. package/src/trading-host.ts +8 -0
  76. package/styles/trading-controls.css +356 -0
  77. package/translation-keys.json +70 -1
@@ -34,10 +34,9 @@ export const OrderStates = {
34
34
  Cancelled: 7,
35
35
  } as const;
36
36
 
37
- /// What the blotter needs beyond the host port. All required: a panel that
38
- /// cannot cancel, modify or reload its orders is not this panel.
39
- export interface ActiveOrdersDeps {
40
- host: TradingHost;
37
+ /// The gestures that reach a venue. All required together: a panel that can
38
+ /// cancel one order but not all of them is a panel half-wired.
39
+ export interface ActiveOrdersActions {
41
40
  cancelOrder(orderId: number): void;
42
41
  dismissOrder(orderId: number): void;
43
42
  editOrderField(orderId: number, field: string): void;
@@ -46,6 +45,18 @@ export interface ActiveOrdersDeps {
46
45
  refreshOrders(): void;
47
46
  }
48
47
 
48
+ /// What the blotter needs beyond the host port, in one of two shapes.
49
+ ///
50
+ /// A live blotter carries every action, because a panel that cannot cancel,
51
+ /// modify or reload its orders is not that panel. A read-only one carries none:
52
+ /// over a finished run - a backtest report, an archived session - there is no
53
+ /// venue left to reach, and the gestures that would reach it are not rendered
54
+ /// rather than rendered and wired to nothing. The union states that, so a live
55
+ /// consumer still cannot omit an action by accident.
56
+ export type ActiveOrdersDeps =
57
+ | ({ host: TradingHost; readOnly?: false } & ActiveOrdersActions)
58
+ | { host: TradingHost; readOnly: true };
59
+
49
60
  export class ActiveOrdersWidget {
50
61
  static TYPE = ControlTypes.ActiveOrders;
51
62
 
@@ -54,6 +65,8 @@ export class ActiveOrdersWidget {
54
65
  // `//` rather than `///` from here down — see the note in positions-widget.
55
66
  _host: TradingHost;
56
67
  _deps: ActiveOrdersDeps;
68
+ _readOnly: boolean;
69
+ _actions: ActiveOrdersActions | null;
57
70
  _closeBtn: HTMLElement | null;
58
71
  _cancelAllBtn: HTMLElement | null;
59
72
  _refreshBtn: HTMLElement | null;
@@ -66,7 +79,7 @@ export class ActiveOrdersWidget {
66
79
  // host, so a missing host has to fail here rather than render a panel
67
80
  // captioned with raw English keys.
68
81
  const host = assertHost(deps?.host, 'ActiveOrdersWidget');
69
- const root = ActiveOrdersWidget._buildRoot(host);
82
+ const root = ActiveOrdersWidget._buildRoot(host, deps?.readOnly === true);
70
83
  root.id = makePanelId(ActiveOrdersWidget.TYPE);
71
84
  hostEl.appendChild(root);
72
85
  return new ActiveOrdersWidget(root, state || {}, deps);
@@ -83,7 +96,7 @@ export class ActiveOrdersWidget {
83
96
  // already give the identical button — and are wired to deps. Cancel-all
84
97
  // keeps a distinct accessible name from its tooltip, which is why it states
85
98
  // `aria-label` rather than taking the tooltip for both.
86
- static _buildRoot(host: TradingHost): HTMLElement {
99
+ static _buildRoot(host: TradingHost, readOnly = false): HTMLElement {
87
100
  return makePanelRoot('active-orders-panel', host.t('ActiveOrders'), [
88
101
  makeElement('div', 'panel-header', {}, [
89
102
  makeElement('span', '', {}, [host.t('OpenOrders')]),
@@ -97,9 +110,11 @@ export class ActiveOrdersWidget {
97
110
  ]),
98
111
  ]),
99
112
  makeElement('div', 'panel-rail', { role: 'toolbar', 'aria-label': host.t('ActiveOrdersActions') }, [
100
- makeIconButton('bt-icon-btn bt-icon-cancel-all panel-cancel-all-btn', host.t('CancelAll'), 'bi-x-circle',
101
- { 'aria-label': host.t('CancelAllOrders') }),
102
- makeIconButton('bt-icon-btn panel-refresh-btn', host.t('Refresh'), 'bi-arrow-clockwise', {}),
113
+ ...(readOnly ? [] : [
114
+ makeIconButton('bt-icon-btn bt-icon-cancel-all panel-cancel-all-btn', host.t('CancelAll'), 'bi-x-circle',
115
+ { 'aria-label': host.t('CancelAllOrders') }),
116
+ makeIconButton('bt-icon-btn panel-refresh-btn', host.t('Refresh'), 'bi-arrow-clockwise', {}),
117
+ ]),
103
118
  makeIconButton('bt-icon-btn panel-export-btn', host.t('ExportToExcel'), 'bi-file-earmark-spreadsheet', {}),
104
119
  ]),
105
120
  ]),
@@ -108,9 +123,16 @@ export class ActiveOrdersWidget {
108
123
 
109
124
  constructor(rootEl: HTMLElement, _state: Record<string, unknown>, deps: ActiveOrdersDeps) {
110
125
  this._host = assertHost(deps?.host, 'ActiveOrdersWidget');
111
- for (const name of ['cancelOrder', 'dismissOrder', 'editOrderField', 'replaceOrder', 'cancelAllOrders', 'refreshOrders'] as const) {
112
- if (typeof deps?.[name] !== 'function')
113
- throw new Error(`ActiveOrdersWidget: dep "${name}" is required`);
126
+ this._readOnly = deps?.readOnly === true;
127
+ if (this._readOnly) {
128
+ this._actions = null;
129
+ } else {
130
+ const actions = deps as ActiveOrdersActions;
131
+ for (const name of ['cancelOrder', 'dismissOrder', 'editOrderField', 'replaceOrder', 'cancelAllOrders', 'refreshOrders'] as const) {
132
+ if (typeof actions?.[name] !== 'function')
133
+ throw new Error(`ActiveOrdersWidget: dep "${name}" is required`);
134
+ }
135
+ this._actions = actions;
114
136
  }
115
137
 
116
138
  this.rootEl = rootEl;
@@ -129,12 +151,12 @@ export class ActiveOrdersWidget {
129
151
 
130
152
  this._cancelAllBtn?.addEventListener('click', (e) => {
131
153
  e.preventDefault();
132
- this._deps.cancelAllOrders();
154
+ this._actions?.cancelAllOrders();
133
155
  });
134
156
 
135
157
  this._refreshBtn?.addEventListener('click', (e) => {
136
158
  e.preventDefault();
137
- this._deps.refreshOrders();
159
+ this._actions?.refreshOrders();
138
160
  });
139
161
 
140
162
  this._exportBtn?.addEventListener('click', (e) => {
@@ -246,7 +268,7 @@ export class ActiveOrdersWidget {
246
268
  if (newVal === currentValue) { this.update(this._orders); return; }
247
269
  const updated = { quantity: order.quantity, limitPrice: order.limitPrice, stopPrice: order.stopPrice };
248
270
  updated[field] = newVal;
249
- this._deps.replaceOrder(orderId, updated.quantity!, updated.limitPrice!, updated.stopPrice!);
271
+ this._actions?.replaceOrder(orderId, updated.quantity!, updated.limitPrice!, updated.stopPrice!);
250
272
  };
251
273
  input.addEventListener('blur', commit);
252
274
  input.addEventListener('keydown', (e) => {
@@ -305,7 +327,7 @@ export class ActiveOrdersWidget {
305
327
  exportable: true,
306
328
  value: (o) => o.quantity,
307
329
  render: (o) => formatQty(o.quantity),
308
- cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'quantity'),
330
+ cellClass: (o) => this._readOnly ? '' : ActiveOrdersWidget._editableClass(o, 'quantity'),
309
331
  bindCell: (td, o) => this._bindInlineEdit(td, o, 'quantity'),
310
332
  },
311
333
  {
@@ -314,7 +336,7 @@ export class ActiveOrdersWidget {
314
336
  exportable: true,
315
337
  value: (o) => o.limitPrice,
316
338
  render: (o) => o.limitPrice ? formatPrice(o.limitPrice) : label('MKT'),
317
- cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'limitPrice'),
339
+ cellClass: (o) => this._readOnly ? '' : ActiveOrdersWidget._editableClass(o, 'limitPrice'),
318
340
  bindCell: (td, o) => this._bindInlineEdit(td, o, 'limitPrice'),
319
341
  exportValue: (o) => o.limitPrice ?? label('MKT'),
320
342
  },
@@ -324,7 +346,7 @@ export class ActiveOrdersWidget {
324
346
  exportable: true,
325
347
  value: (o) => o.stopPrice,
326
348
  render: (o) => o.stopPrice ? formatPrice(o.stopPrice) : '--',
327
- cellClass: (o) => ActiveOrdersWidget._editableClass(o, 'stopPrice'),
349
+ cellClass: (o) => this._readOnly ? '' : ActiveOrdersWidget._editableClass(o, 'stopPrice'),
328
350
  bindCell: (td, o) => this._bindInlineEdit(td, o, 'stopPrice'),
329
351
  exportValue: (o) => o.stopPrice ?? '',
330
352
  },
@@ -336,14 +358,14 @@ export class ActiveOrdersWidget {
336
358
  render: (o) => this._statusCell(o),
337
359
  exportValue: (o) => presentation.statusText(o.status!),
338
360
  },
339
- {
361
+ ...(this._readOnly ? [] : [{
340
362
  key: 'actions',
341
363
  header: label('Actions'),
342
364
  headerHidden: true,
343
365
  exportable: false,
344
366
  cellClass: () => 'position-actions',
345
- render: (o) => this._actionButton(o),
346
- },
367
+ render: (o: OrderRow) => this._actionButton(o),
368
+ } as GridColumn<OrderRow>]),
347
369
  ];
348
370
  }
349
371
 
@@ -369,8 +391,9 @@ export class ActiveOrdersWidget {
369
391
  // Double-click to edit belongs to the whole cell rather than to a control
370
392
  // inside it, which is why it is wired here instead of returned by render().
371
393
  _bindInlineEdit(td: HTMLTableCellElement, order: OrderRow, field: 'quantity' | 'limitPrice' | 'stopPrice'): void {
372
- if (!ActiveOrdersWidget._canEdit(order, field)) return;
373
- td.addEventListener('dblclick', () => this._deps.editOrderField(order.id!, field));
394
+ if (this._actions === null || !ActiveOrdersWidget._canEdit(order, field)) return;
395
+ const edit = this._actions.editOrderField;
396
+ td.addEventListener('dblclick', () => edit(order.id!, field));
374
397
  }
375
398
 
376
399
  // Status text, plus a hoverable icon carrying the rejection reason. Some
@@ -412,8 +435,9 @@ export class ActiveOrdersWidget {
412
435
  icon.className = 'bi bi-x-circle';
413
436
  button.appendChild(icon);
414
437
  button.addEventListener('click', () => {
415
- if (isTerminal) this._deps.dismissOrder(order.id!);
416
- else this._deps.cancelOrder(order.id!);
438
+ if (this._actions === null) return;
439
+ if (isTerminal) this._actions.dismissOrder(order.id!);
440
+ else this._actions.cancelOrder(order.id!);
417
441
  });
418
442
  return button;
419
443
  }
@@ -0,0 +1,199 @@
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
+
16
+ export const OptionTypes = {
17
+ Call: 'call',
18
+ Put: 'put',
19
+ } as const;
20
+
21
+ export type OptionType = typeof OptionTypes[keyof typeof OptionTypes];
22
+
23
+ /// Everything a price depends on. Time is in years, rates and volatility are fractions - 0.05
24
+ /// is five percent, not five.
25
+ export interface OptionInputs {
26
+ assetPrice: number;
27
+ strike: number;
28
+ /// Years remaining. Zero once expired, which makes every greek zero and the premium
29
+ /// intrinsic.
30
+ timeToExpiry: number;
31
+ riskFree: number;
32
+ dividend: number;
33
+ /// Volatility, as a fraction.
34
+ deviation: number;
35
+ }
36
+
37
+ export interface Greeks {
38
+ delta: number;
39
+ gamma: number;
40
+ /// Per one point of volatility.
41
+ vega: number;
42
+ /// Per calendar day.
43
+ theta: number;
44
+ /// Per one point of rate.
45
+ rho: number;
46
+ }
47
+
48
+ const SQRT_2PI = Math.sqrt(2 * Math.PI);
49
+ const DAYS_IN_YEAR = 365;
50
+
51
+ /// The standard normal CDF.
52
+ ///
53
+ /// Hart's rational approximation, which is what pricing libraries use and what a desk's numbers
54
+ /// are expected to agree with: accurate to about 1e-15 across the range, and exactly a half at
55
+ /// the mean rather than nearly so. The cheaper Abramowitz-and-Stegun fit is good to 1e-7, which
56
+ /// sounds like enough until a greek that should be symmetric is not.
57
+ export function normalCdf(x: number): number {
58
+ const z = Math.abs(x);
59
+
60
+ // Beyond this the tail is smaller than double precision can carry anyway.
61
+ if (z > 37) return x > 0 ? 1 : 0;
62
+
63
+ const e = Math.exp(-(z * z) / 2);
64
+ let tail: number;
65
+
66
+ if (z < 7.07106781186547) {
67
+ let b = 3.52624965998911e-02 * z + 0.700383064443688;
68
+ b = b * z + 6.37396220353165;
69
+ b = b * z + 33.912866078383;
70
+ b = b * z + 112.079291497871;
71
+ b = b * z + 221.213596169931;
72
+ b = b * z + 220.206867912376;
73
+
74
+ let d = 8.83883476483184e-02 * z + 1.75566716318264;
75
+ d = d * z + 16.064177579207;
76
+ d = d * z + 86.7807322029461;
77
+ d = d * z + 296.564248779674;
78
+ d = d * z + 637.333633378831;
79
+ d = d * z + 793.826512519948;
80
+ d = d * z + 440.413735824752;
81
+
82
+ tail = (e * b) / d;
83
+ } else {
84
+ // A continued fraction, which is what stays accurate once the polynomial ratio above
85
+ // starts losing digits to cancellation.
86
+ let b = z + 0.65;
87
+ b = z + 4 / b;
88
+ b = z + 3 / b;
89
+ b = z + 2 / b;
90
+ b = z + 1 / b;
91
+ tail = e / (b * 2.506628274631);
92
+ }
93
+
94
+ return x > 0 ? 1 - tail : tail;
95
+ }
96
+
97
+ /// The standard normal density.
98
+ export function normalPdf(x: number): number {
99
+ return Math.exp(-0.5 * x * x) / SQRT_2PI;
100
+ }
101
+
102
+ /// d1, or zero when there is no time or no volatility for the division to mean anything.
103
+ export function d1(inputs: OptionInputs): number {
104
+ const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
105
+ const spread = deviation * Math.sqrt(timeToExpiry);
106
+
107
+ if (spread === 0 || assetPrice <= 0 || strike <= 0) return 0;
108
+
109
+ return (Math.log(assetPrice / strike) + (riskFree - dividend + (deviation * deviation) / 2) * timeToExpiry) / spread;
110
+ }
111
+
112
+ /// d2, which is d1 less one standard deviation of the remaining time.
113
+ export function d2(inputs: OptionInputs): number {
114
+ const spread = inputs.deviation * Math.sqrt(inputs.timeToExpiry);
115
+ return spread === 0 ? 0 : d1(inputs) - spread;
116
+ }
117
+
118
+ /// What the option is worth.
119
+ ///
120
+ /// At expiry, or with no volatility, that is its intrinsic value - the formula degenerates to
121
+ /// exactly that, and saying so explicitly keeps a zero denominator out of the general case.
122
+ export function premium(type: OptionType, inputs: OptionInputs): number {
123
+ const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
124
+
125
+ if (timeToExpiry <= 0 || deviation <= 0)
126
+ return Math.max(0, type === OptionTypes.Call ? assetPrice - strike : strike - assetPrice);
127
+
128
+ const a = d1(inputs);
129
+ const b = d2(inputs);
130
+ const carried = assetPrice * Math.exp(-dividend * timeToExpiry);
131
+ const discounted = strike * Math.exp(-riskFree * timeToExpiry);
132
+
133
+ return type === OptionTypes.Call
134
+ ? carried * normalCdf(a) - discounted * normalCdf(b)
135
+ : discounted * normalCdf(-b) - carried * normalCdf(-a);
136
+ }
137
+
138
+ /// Every greek at once: they share d1, and computing them together is both cheaper and the only
139
+ /// way they are guaranteed to describe the same moment.
140
+ export function greeks(type: OptionType, inputs: OptionInputs): Greeks {
141
+ const { assetPrice, strike, timeToExpiry, riskFree, dividend, deviation } = inputs;
142
+ const sign = type === OptionTypes.Call ? 1 : -1;
143
+
144
+ if (timeToExpiry <= 0 || deviation <= 0 || assetPrice <= 0) {
145
+ // An expired option still has a delta - it is one or nothing, depending on which side of
146
+ // the strike it finished. Everything else has stopped moving.
147
+ const inTheMoney = type === OptionTypes.Call ? assetPrice > strike : assetPrice < strike;
148
+ return { delta: inTheMoney ? sign : 0, gamma: 0, vega: 0, theta: 0, rho: 0 };
149
+ }
150
+
151
+ const a = d1(inputs);
152
+ const b = d2(inputs);
153
+ const sqrtT = Math.sqrt(timeToExpiry);
154
+ const density = normalPdf(a);
155
+ const carry = Math.exp(-dividend * timeToExpiry);
156
+ const discount = Math.exp(-riskFree * timeToExpiry);
157
+
158
+ const delta = sign * carry * normalCdf(sign * a);
159
+ const gamma = (carry * density) / (assetPrice * deviation * sqrtT);
160
+
161
+ // Scaled the way a desk quotes them: vega per one volatility point, rho per one rate point,
162
+ // theta per calendar day.
163
+ const vega = assetPrice * carry * density * sqrtT * 0.01;
164
+ const rho = sign * strike * timeToExpiry * discount * normalCdf(sign * b) * 0.01;
165
+
166
+ const theta = (
167
+ -(assetPrice * carry * density * deviation) / (2 * sqrtT)
168
+ - sign * riskFree * strike * discount * normalCdf(sign * b)
169
+ + sign * dividend * assetPrice * carry * normalCdf(sign * a)
170
+ ) / DAYS_IN_YEAR;
171
+
172
+ return { delta, gamma, vega, theta, rho };
173
+ }
174
+
175
+ /// The volatility that would produce this price, or null when none would.
176
+ ///
177
+ /// Bisection rather than Newton: vega collapses far from the money and deep in time, and a
178
+ /// Newton step divided by a vanishing vega walks off to nonsense. Halving cannot, and forty
179
+ /// steps over a range this wide is finer than any quote.
180
+ export function impliedVolatility(type: OptionType, inputs: OptionInputs, price: number): number | null {
181
+ if (!(price > 0) || inputs.timeToExpiry <= 0) return null;
182
+
183
+ const at = (deviation: number): number => premium(type, { ...inputs, deviation });
184
+
185
+ let low = 1e-6;
186
+ let high = 5;
187
+
188
+ // A price outside what any volatility in that range can produce has no answer, and
189
+ // returning the nearest bound would be a number that means nothing.
190
+ if (price < at(low) || price > at(high)) return null;
191
+
192
+ for (let step = 0; step < 60; step++) {
193
+ const mid = (low + high) / 2;
194
+ if (at(mid) < price) low = mid;
195
+ else high = mid;
196
+ }
197
+
198
+ return (low + high) / 2;
199
+ }
@@ -22,6 +22,10 @@ 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
  } as const;
26
30
 
27
31
  export type ControlType = typeof ControlTypes[keyof typeof ControlTypes];
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  OrderType,
22
22
  PositionRow,
23
23
  QuoteStats,
24
+ StatisticRow,
24
25
  TradeRow,
25
26
  } from './trading-data.js';
26
27
 
@@ -67,6 +68,41 @@ export type { BookLevel, OrderBookFrame, QuoteLevel } from './trading-data.js';
67
68
  // where a class name cannot reach.
68
69
  export type { CanvasPalette } from './trading-host.js';
69
70
 
71
+ // The cumulative P&L curve, shared by everything that draws one: a strategy row's
72
+ // sparkline and a statistics card are the same curve at two sizes. A backtest's own
73
+ // equity is not here - that one shares the price axis and belongs to the chart engine.
74
+ export { compressPnl, drawPnlCurve, pnlCurve } from './pnl-curve.js';
75
+
76
+ export type { PnlBox, PnlCurve, PnlCurveContext, PnlCurveStyle, PnlPoint } from './pnl-curve.js';
77
+
78
+ export { OptionDeskWidget, greekPlaces, greekScales, scaleChain, sideGreeks } from './option-desk-widget.js';
79
+
80
+ export type { OptionChainContext, OptionDeskDeps, OptionSide, OptionStrike } from './option-desk-widget.js';
81
+
82
+ // Option pricing, for a host that has quotes and an expiry but no pricing service. A host
83
+ // that computes its own greeks sends them instead and none of this is reached.
84
+ export { OptionTypes, d1, d2, greeks, impliedVolatility, normalCdf, normalPdf, premium } from './black-scholes.js';
85
+
86
+ export type { Greeks, OptionInputs, OptionType } from './black-scholes.js';
87
+
88
+ export { StrategiesWidget, StrategyStates } from './strategies-widget.js';
89
+
90
+ export type { StrategiesActions, StrategiesDeps, StrategyRow, StrategyState } from './strategies-widget.js';
91
+
92
+ export { LogMonitorWidget } from './log-monitor-widget.js';
93
+
94
+ export type { LogMonitorDeps } from './log-monitor-widget.js';
95
+
96
+ // The log's own shapes: how sources nest, and which messages a view of them keeps. A host
97
+ // rendering its own view of the same feed reads these rather than reinventing the filter.
98
+ export { LogLevels, buildLogTree, keepLog, subtreeOf } from './log-tree.js';
99
+
100
+ export type { LogLevel, LogMessageRow, LogSourceNode, LogTreeNode, LogView } from './log-tree.js';
101
+
102
+ export { StatisticsWidget, formatStatistic } from './statistics-widget.js';
103
+
104
+ export type { StatisticsDeps } from './statistics-widget.js';
105
+
70
106
  export { TradeFeedWidget } from './tradefeed-widget.js';
71
107
 
72
108
  export type { TradeFeedDeps } from './tradefeed-widget.js';
@@ -81,3 +117,8 @@ export type { FeedBubble, FeedTick } from './tradefeed-aggregator.js';
81
117
  export { layoutBubbles } from './tradefeed-bubbles.js';
82
118
 
83
119
  export type { BubbleAxisTick, BubbleLane, BubbleLaneShape, BubbleLayout, BubbleLayoutInput, BubbleShape } from './tradefeed-bubbles.js';
120
+
121
+ // The grid's own menu, worded by the host. Exported because a host that builds a table of its
122
+ // own - a run's log beside the blotters, say - needs the same menu in the same words, and the
123
+ // alternative is every consumer restating thirty labels the package already knows.
124
+ export { makeGridMenu } from './grid-menu.js';