@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.
- package/README.md +95 -16
- package/dist/esm/active-orders-widget.js +38 -25
- package/dist/esm/active-orders-widget.js.map +1 -1
- 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/esm/trade-history-widget.js +14 -6
- package/dist/esm/trade-history-widget.js.map +1 -1
- package/dist/esm/tradefeed-widget.js +3 -3
- package/dist/esm/tradefeed-widget.js.map +1 -1
- package/dist/esm/trading-host.js +1 -0
- package/dist/esm/trading-host.js.map +1 -1
- package/dist/sstradingcontrols.js +1358 -78
- package/dist/sstradingcontrols.js.map +4 -4
- package/dist/types/active-orders-widget.d.ts +11 -3
- package/dist/types/active-orders-widget.d.ts.map +1 -1
- 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/trade-history-widget.d.ts +3 -1
- package/dist/types/trade-history-widget.d.ts.map +1 -1
- package/dist/types/trading-data.d.ts +9 -0
- package/dist/types/trading-data.d.ts.map +1 -1
- package/dist/types/trading-host.d.ts +1 -0
- package/dist/types/trading-host.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/active-orders-widget.ts +49 -25
- 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/trade-history-widget.ts +19 -6
- package/src/tradefeed-widget.ts +3 -3
- package/src/trading-data.ts +22 -0
- package/src/trading-host.ts +8 -0
- package/styles/trading-controls.css +356 -0
- package/translation-keys.json +70 -1
|
@@ -34,10 +34,9 @@ export const OrderStates = {
|
|
|
34
34
|
Cancelled: 7,
|
|
35
35
|
} as const;
|
|
36
36
|
|
|
37
|
-
///
|
|
38
|
-
///
|
|
39
|
-
export interface
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
154
|
+
this._actions?.cancelAllOrders();
|
|
133
155
|
});
|
|
134
156
|
|
|
135
157
|
this._refreshBtn?.addEventListener('click', (e) => {
|
|
136
158
|
e.preventDefault();
|
|
137
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
416
|
-
|
|
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
|
+
}
|
package/src/control-types.ts
CHANGED
|
@@ -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';
|