@stocksharp/trading-controls 1.3.0 → 1.4.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 (62) hide show
  1. package/README.md +113 -5
  2. package/dist/esm/chart-engine.js +15 -0
  3. package/dist/esm/chart-engine.js.map +1 -0
  4. package/dist/esm/control-types.js +4 -0
  5. package/dist/esm/control-types.js.map +1 -1
  6. package/dist/esm/equity-widget.js +261 -0
  7. package/dist/esm/equity-widget.js.map +1 -0
  8. package/dist/esm/heatmap-grid.js +271 -0
  9. package/dist/esm/heatmap-grid.js.map +1 -0
  10. package/dist/esm/index.js +19 -0
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/optimization-heatmap-widget.js +303 -0
  13. package/dist/esm/optimization-heatmap-widget.js.map +1 -0
  14. package/dist/esm/option-smile-widget.js +252 -0
  15. package/dist/esm/option-smile-widget.js.map +1 -0
  16. package/dist/esm/pnl-curve.js +5 -1
  17. package/dist/esm/pnl-curve.js.map +1 -1
  18. package/dist/esm/surface-grid.js +282 -0
  19. package/dist/esm/surface-grid.js.map +1 -0
  20. package/dist/esm/surface-widget.js +379 -0
  21. package/dist/esm/surface-widget.js.map +1 -0
  22. package/dist/sstradingcontrols.js +1595 -168
  23. package/dist/sstradingcontrols.js.map +4 -4
  24. package/dist/types/chart-engine.d.ts +3 -0
  25. package/dist/types/chart-engine.d.ts.map +1 -0
  26. package/dist/types/control-types.d.ts +4 -0
  27. package/dist/types/control-types.d.ts.map +1 -1
  28. package/dist/types/equity-widget.d.ts +34 -0
  29. package/dist/types/equity-widget.d.ts.map +1 -0
  30. package/dist/types/heatmap-grid.d.ts +88 -0
  31. package/dist/types/heatmap-grid.d.ts.map +1 -0
  32. package/dist/types/index.d.ts +15 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/optimization-heatmap-widget.d.ts +45 -0
  35. package/dist/types/optimization-heatmap-widget.d.ts.map +1 -0
  36. package/dist/types/option-smile-widget.d.ts +39 -0
  37. package/dist/types/option-smile-widget.d.ts.map +1 -0
  38. package/dist/types/pnl-curve.d.ts +3 -0
  39. package/dist/types/pnl-curve.d.ts.map +1 -1
  40. package/dist/types/surface-grid.d.ts +64 -0
  41. package/dist/types/surface-grid.d.ts.map +1 -0
  42. package/dist/types/surface-widget.d.ts +64 -0
  43. package/dist/types/surface-widget.d.ts.map +1 -0
  44. package/package.json +28 -2
  45. package/screenshots/equity.png +0 -0
  46. package/screenshots/optimization-surface.png +0 -0
  47. package/screenshots/optimization.png +0 -0
  48. package/screenshots/option-smile.png +0 -0
  49. package/screenshots/panels.jpg +0 -0
  50. package/src/chart-engine.global.ts +64 -0
  51. package/src/chart-engine.ts +29 -0
  52. package/src/control-types.ts +4 -0
  53. package/src/equity-widget.ts +289 -0
  54. package/src/heatmap-grid.ts +405 -0
  55. package/src/index.ts +46 -0
  56. package/src/optimization-heatmap-widget.ts +347 -0
  57. package/src/option-smile-widget.ts +281 -0
  58. package/src/pnl-curve.ts +13 -1
  59. package/src/surface-grid.ts +410 -0
  60. package/src/surface-widget.ts +457 -0
  61. package/styles/trading-controls.css +281 -0
  62. package/translation-keys.json +14 -1
@@ -0,0 +1,405 @@
1
+ // The geometry of a heatmap - one metric over two discrete axes - as pure functions over numbers.
2
+ //
3
+ // A heatmap is a lattice: every distinct value of one axis crossed with every distinct value of
4
+ // the other, one cell per pair, coloured by what that pair measured. What produced the pairs is
5
+ // not here and does not need to be - an optimisation sweep is one source of them, and the object
6
+ // is the same shape whatever the source.
7
+ //
8
+ // None of this needs a canvas, which is what makes it checkable: a fake 2D context can only
9
+ // record the calls a control made, so the numbers those calls carry are checked here, against
10
+ // arithmetic rather than against pixels.
11
+ //
12
+ // The colour comes out as a signed number in -1..1 rather than as a colour. The sign says which
13
+ // side of the anchor a cell fell on and the magnitude how far, and the control turns that into
14
+ // one of the two direction colours its host answered with. Nothing in this file knows a colour.
15
+
16
+ /// Which way is better. Stated wherever a scale is built, never inferred: a metric does not
17
+ /// carry its own direction, and a map of drawdowns would otherwise paint its worst corner in
18
+ /// the winning colour.
19
+ export const HeatDirections = {
20
+ Higher: 'higher',
21
+ Lower: 'lower',
22
+ } as const;
23
+
24
+ export type HeatDirection = typeof HeatDirections[keyof typeof HeatDirections];
25
+
26
+ /// One measurement: a pair of axis values, and what the metric was there. The axis values are
27
+ /// strings because an axis is discrete - "10", "00:05:00" and "True" are all positions on one,
28
+ /// and the only arithmetic they take part in is their order.
29
+ export interface HeatCell {
30
+ x: string;
31
+ y: string;
32
+ value: number;
33
+ }
34
+
35
+ /// Every measurement at one pair, folded into one. Two runs at the same pair are two samples of
36
+ /// the same cell, so the cell is their mean and says how many it is the mean of: a mean over
37
+ /// five runs and a single run are not the same evidence, and a map that dropped the count would
38
+ /// read as if they were.
39
+ export interface HeatBucket {
40
+ x: string;
41
+ y: string;
42
+ value: number;
43
+ count: number;
44
+ }
45
+
46
+ export interface HeatRect {
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ }
52
+
53
+ /// What the colours are measured against.
54
+ export interface HeatScale {
55
+ /// The value the colour turns over at - where a cell is neither good nor bad.
56
+ anchor: number;
57
+ /// Distance from the anchor to full colour, the same on both sides. Never zero: a sweep
58
+ /// whose runs all measured the same thing has nothing to shade, and dividing by the spread
59
+ /// it does not have would make every cell a NaN instead of a neutral one.
60
+ reach: number;
61
+ min: number;
62
+ max: number;
63
+ }
64
+
65
+ export interface HeatCellShape {
66
+ bucket: HeatBucket;
67
+ rect: HeatRect;
68
+ /// -1..1. The sign says which side of the anchor the cell fell on, the magnitude how far.
69
+ tint: number;
70
+ /// The one cell the metric says is best. Exactly one per map.
71
+ best: boolean;
72
+ }
73
+
74
+ /// A pair the sweep never ran. It keeps its place in the lattice - a missing row would slide
75
+ /// every cell after it - and carries no value, because there is none.
76
+ export interface HeatGapShape {
77
+ x: string;
78
+ y: string;
79
+ rect: HeatRect;
80
+ }
81
+
82
+ /// A string drawn at a point. Where the text anchors is geometry; which end it anchors by is
83
+ /// fixed per group and stated by the control that draws it.
84
+ export interface HeatLabel {
85
+ text: string;
86
+ x: number;
87
+ y: number;
88
+ }
89
+
90
+ /// A number drawn at a point. Unformatted on purpose: how a metric reads is the control's
91
+ /// business, and a formatter passed in here would be policy hiding in the arithmetic.
92
+ export interface HeatMark {
93
+ value: number;
94
+ x: number;
95
+ y: number;
96
+ }
97
+
98
+ export interface HeatLegendStep {
99
+ rect: HeatRect;
100
+ tint: number;
101
+ }
102
+
103
+ /// The key the map is read against. The anchor is labelled as well as the two ends, because
104
+ /// where the colour turns over is not guessable from the picture: on a sweep that never lost,
105
+ /// neutral is the middle of what was tried and not zero.
106
+ export interface HeatLegend {
107
+ steps: HeatLegendStep[];
108
+ low: HeatMark;
109
+ anchor: HeatMark;
110
+ high: HeatMark;
111
+ }
112
+
113
+ export interface HeatLayoutInput {
114
+ /// The box one paint fills, in the units the control draws in.
115
+ width: number;
116
+ height: number;
117
+ cells: readonly HeatCell[];
118
+ betterWhen: HeatDirection;
119
+ xLabel: string;
120
+ yLabel: string;
121
+ }
122
+
123
+ export interface HeatLayout {
124
+ /// The lattice itself, inside the gutters the labels need.
125
+ plot: HeatRect;
126
+ /// The axis values, in the order they are laid out: columns left to right, rows bottom up.
127
+ columns: string[];
128
+ rows: string[];
129
+ /// Row-major from the foot of the axis, which is both draw order and scan order.
130
+ cells: HeatCellShape[];
131
+ gaps: HeatGapShape[];
132
+ xTicks: HeatLabel[];
133
+ yTicks: HeatLabel[];
134
+ xTitle: HeatLabel;
135
+ yTitle: HeatLabel;
136
+ legend: HeatLegend;
137
+ scale: HeatScale;
138
+ }
139
+
140
+ // Proportions, not looks: a canvas has no cascade to read them from, and moving them into CSS
141
+ // would only mean reading them back out again.
142
+ const LEFT_GUTTER = 54;
143
+ const RIGHT_PAD = 6;
144
+ const TOP_BAND = 26;
145
+ const BOTTOM_BAND = 30;
146
+ const TICK_GAP = 4;
147
+ const LABEL_HEIGHT = 13;
148
+ /// Half a pixel off each side, so abutting cells show a hairline of the panel between them and
149
+ /// the lattice reads as cells rather than as one continuous wash.
150
+ const CELL_INSET = 0.5;
151
+ const LEGEND_WIDTH = 140;
152
+ const LEGEND_HEIGHT = 8;
153
+ const LEGEND_Y = 14;
154
+ const LEGEND_STEPS = 28;
155
+ /// Room one label wants. Below this the labels touch, and an axis whose values overprint each
156
+ /// other is worse than an axis showing every fourth value.
157
+ const MIN_TICK_WIDTH = 34;
158
+ const MIN_TICK_HEIGHT = 13;
159
+
160
+ /// A value that is a number written as text, trailing unit included: "12", "-0.5", "7.5%".
161
+ const NUMERIC = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*%?$/;
162
+
163
+ /// Every run at a pair, as one cell.
164
+ ///
165
+ /// A run whose metric is not a number is dropped rather than folded in: a report that carries a
166
+ /// missing figure as an empty string would otherwise turn the whole cell into a NaN, and a NaN
167
+ /// cell paints as nothing while claiming the pair was measured.
168
+ export function foldCells(cells: readonly HeatCell[]): HeatBucket[] {
169
+ const folded = new Map<string, { x: string; y: string; sum: number; count: number }>();
170
+ const order: string[] = [];
171
+
172
+ for (const cell of cells) {
173
+ if (cell === null || cell === undefined || !Number.isFinite(cell.value)) continue;
174
+ const id = pairKey(cell.x, cell.y);
175
+ const found = folded.get(id);
176
+ if (found === undefined) {
177
+ folded.set(id, { x: cell.x, y: cell.y, sum: cell.value, count: 1 });
178
+ order.push(id);
179
+ } else {
180
+ found.sum += cell.value;
181
+ found.count += 1;
182
+ }
183
+ }
184
+
185
+ return order.map(id => {
186
+ const fold = folded.get(id)!;
187
+ return { x: fold.x, y: fold.y, value: fold.sum / fold.count, count: fold.count };
188
+ });
189
+ }
190
+
191
+ /// One axis, in the order it is read.
192
+ ///
193
+ /// Numerically when every value on it is a number, which is the whole point: a text sort puts
194
+ /// "10" before "2" and the axis stops being monotonic, so the map's shape becomes an artefact of
195
+ /// how the values happened to be spelled. Text order otherwise - and it is the right answer for
196
+ /// the shapes that actually arrive, because .NET writes a time span at a fixed width, so
197
+ /// "00:05:00" < "00:15:00" < "01:00:00" is also chronological.
198
+ export function axisValues(values: readonly string[]): string[] {
199
+ const unique: string[] = [];
200
+ const seen = new Set<string>();
201
+ for (const value of values) {
202
+ if (seen.has(value)) continue;
203
+ seen.add(value);
204
+ unique.push(value);
205
+ }
206
+
207
+ if (unique.every(value => NUMERIC.test(value)))
208
+ return unique.sort((left, right) => numberOf(left) - numberOf(right));
209
+
210
+ return unique.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
211
+ }
212
+
213
+ /// What the colours are measured against.
214
+ ///
215
+ /// Two anchors, and which one applies is decided by the data rather than by the caller. When the
216
+ /// values straddle zero, zero is the anchor: profit and loss are different in kind, not merely
217
+ /// in degree, and a map that shaded a loss green because it was less bad than the others would
218
+ /// be lying about the only thing a reader checks first. When zero is nowhere in the range - a
219
+ /// Sharpe of 1.8 to 2.2, a profit of 100 to 5000 - the anchor is the middle of the range,
220
+ /// because anchoring at a zero the sweep never came near produces one flat block with no
221
+ /// contrast, and comparing the cells with each other is what the reader came for.
222
+ ///
223
+ /// The reach is the same on both sides of the anchor, so one unit of colour means one amount of
224
+ /// metric everywhere on the map. Scaling each side to its own extreme instead would make a five
225
+ /// dollar loss look exactly as red as a five hundred dollar one.
226
+ export function heatScale(buckets: readonly HeatBucket[]): HeatScale {
227
+ let min = Infinity;
228
+ let max = -Infinity;
229
+ for (const bucket of buckets) {
230
+ if (bucket.value < min) min = bucket.value;
231
+ if (bucket.value > max) max = bucket.value;
232
+ }
233
+ if (!isFinite(min) || !isFinite(max)) { min = 0; max = 0; }
234
+
235
+ const anchor = min <= 0 && max >= 0 ? 0 : (min + max) / 2;
236
+ const reach = Math.max(Math.abs(max - anchor), Math.abs(anchor - min));
237
+
238
+ return { anchor, reach: reach > 0 ? reach : 1, min, max };
239
+ }
240
+
241
+ /// How good a value is, as -1..1. Positive is better whichever way better runs.
242
+ export function tintOf(value: number, scale: HeatScale, betterWhen: HeatDirection): number {
243
+ const distance = (value - scale.anchor) / scale.reach;
244
+ const signed = betterWhen === HeatDirections.Lower ? -distance : distance;
245
+ return Math.max(-1, Math.min(1, signed));
246
+ }
247
+
248
+ /// The inverse, for labelling a legend: what value a tint stands for.
249
+ export function valueAt(tint: number, scale: HeatScale, betterWhen: HeatDirection): number {
250
+ const distance = betterWhen === HeatDirections.Lower ? -tint : tint;
251
+ return scale.anchor + distance * scale.reach;
252
+ }
253
+
254
+ /// Lay one map out, or null when nothing was measured.
255
+ export function layoutHeatmap(input: HeatLayoutInput): HeatLayout | null {
256
+ const buckets = foldCells(input.cells);
257
+ if (buckets.length === 0) return null;
258
+
259
+ const columns = axisValues(buckets.map(bucket => bucket.x));
260
+ const rows = axisValues(buckets.map(bucket => bucket.y));
261
+ const scale = heatScale(buckets);
262
+
263
+ const plot: HeatRect = {
264
+ x: LEFT_GUTTER,
265
+ y: TOP_BAND,
266
+ width: Math.max(1, input.width - LEFT_GUTTER - RIGHT_PAD),
267
+ height: Math.max(1, input.height - TOP_BAND - BOTTOM_BAND),
268
+ };
269
+
270
+ const cellWidth = plot.width / columns.length;
271
+ const cellHeight = plot.height / rows.length;
272
+ const rectOf = (column: number, row: number): HeatRect => ({
273
+ x: plot.x + column * cellWidth + CELL_INSET,
274
+ // Row 0 sits at the FOOT of the plot: this is a chart, and a chart's Y grows upward. A
275
+ // table would put it at the top, and then the map and its axis would disagree about
276
+ // which corner is which.
277
+ y: plot.y + plot.height - (row + 1) * cellHeight + CELL_INSET,
278
+ width: Math.max(1, cellWidth - CELL_INSET * 2),
279
+ height: Math.max(1, cellHeight - CELL_INSET * 2),
280
+ });
281
+
282
+ const measured = new Map(buckets.map(bucket => [pairKey(bucket.x, bucket.y), bucket]));
283
+ const cells: HeatCellShape[] = [];
284
+ const gaps: HeatGapShape[] = [];
285
+ for (let row = 0; row < rows.length; row++) {
286
+ for (let column = 0; column < columns.length; column++) {
287
+ const bucket = measured.get(pairKey(columns[column], rows[row]));
288
+ if (bucket === undefined) {
289
+ gaps.push({ x: columns[column], y: rows[row], rect: rectOf(column, row) });
290
+ continue;
291
+ }
292
+ cells.push({
293
+ bucket,
294
+ rect: rectOf(column, row),
295
+ tint: tintOf(bucket.value, scale, input.betterWhen),
296
+ best: false,
297
+ });
298
+ }
299
+ }
300
+
301
+ // The highest tint is the best cell whichever way better runs, which is the reason the tint
302
+ // is signed by direction rather than by the metric. A tie goes to the first in scan order -
303
+ // the cell nearest the origin - rather than to whichever run happened to arrive first.
304
+ let best = cells[0];
305
+ for (const shape of cells) if (shape.tint > best.tint) best = shape;
306
+ best.best = true;
307
+
308
+ const xTicks = tickIndices(columns.length, tickStep(columns.length, plot.width, MIN_TICK_WIDTH))
309
+ .map(index => ({
310
+ text: columns[index],
311
+ x: plot.x + (index + 0.5) * cellWidth,
312
+ y: plot.y + plot.height + TICK_GAP,
313
+ }));
314
+
315
+ const yTicks = tickIndices(rows.length, tickStep(rows.length, plot.height, MIN_TICK_HEIGHT))
316
+ .map(index => ({
317
+ text: rows[index],
318
+ x: plot.x - TICK_GAP,
319
+ y: plot.y + plot.height - (index + 0.5) * cellHeight,
320
+ }));
321
+
322
+ return {
323
+ plot,
324
+ columns,
325
+ rows,
326
+ cells,
327
+ gaps,
328
+ xTicks,
329
+ yTicks,
330
+ xTitle: { text: input.xLabel, x: plot.x + plot.width / 2, y: plot.y + plot.height + TICK_GAP + LABEL_HEIGHT },
331
+ // Above the plot rather than beside it: a title down the left edge would have to be
332
+ // rotated, and a rotation is a transform the control would then have to unwind around
333
+ // everything else it draws.
334
+ yTitle: { text: input.yLabel, x: 0, y: 0 },
335
+ legend: legendOf(plot, scale, input.betterWhen),
336
+ scale,
337
+ };
338
+ }
339
+
340
+ /// The cell under a point, or null. Cells never overlap, so the first match is the only one -
341
+ /// unlike a scatter, where the hit test has to walk the draw order backwards.
342
+ export function hitHeatmap(layout: HeatLayout, x: number, y: number): HeatCellShape | null {
343
+ for (const shape of layout.cells) {
344
+ const rect = shape.rect;
345
+ if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height)
346
+ return shape;
347
+ }
348
+ return null;
349
+ }
350
+
351
+ function legendOf(plot: HeatRect, scale: HeatScale, betterWhen: HeatDirection): HeatLegend {
352
+ const right = plot.x + plot.width;
353
+ const left = Math.max(plot.x, right - LEGEND_WIDTH);
354
+ const stepWidth = (right - left) / LEGEND_STEPS;
355
+
356
+ const steps: HeatLegendStep[] = [];
357
+ for (let step = 0; step < LEGEND_STEPS; step++) {
358
+ steps.push({
359
+ // Half a pixel of overlap: abutting fills leave a seam wherever the boundary is
360
+ // fractional, and a key with a comb of background through it reads as a scale with
361
+ // holes in it.
362
+ rect: { x: left + step * stepWidth, y: LEGEND_Y, width: stepWidth + 0.5, height: LEGEND_HEIGHT },
363
+ tint: -1 + (2 * step) / (LEGEND_STEPS - 1),
364
+ });
365
+ }
366
+
367
+ return {
368
+ steps,
369
+ low: { value: valueAt(-1, scale, betterWhen), x: left, y: 0 },
370
+ anchor: { value: scale.anchor, x: (left + right) / 2, y: 0 },
371
+ high: { value: valueAt(1, scale, betterWhen), x: right, y: 0 },
372
+ };
373
+ }
374
+
375
+ /// How many values to skip between labels so they do not touch.
376
+ function tickStep(count: number, span: number, room: number): number {
377
+ const slots = Math.max(1, Math.floor(span / room));
378
+ return Math.max(1, Math.ceil(count / slots));
379
+ }
380
+
381
+ /// Which values get a label. The last one always does - the end of an axis is what a reader
382
+ /// checks first - and the label before it gives way when the step would land them on top of
383
+ /// each other.
384
+ function tickIndices(count: number, step: number): number[] {
385
+ const indices: number[] = [];
386
+ for (let index = 0; index < count; index += step) indices.push(index);
387
+
388
+ const last = count - 1;
389
+ if (indices[indices.length - 1] !== last) {
390
+ if (last - indices[indices.length - 1] < step) indices.pop();
391
+ indices.push(last);
392
+ }
393
+ return indices;
394
+ }
395
+
396
+ /// A pair as one map key. NUL rather than a printable separator: an axis value may contain any
397
+ /// character a parameter's own notation uses, and "a|b" crossed with "c" must not collide with
398
+ /// "a" crossed with "b|c".
399
+ function pairKey(x: string, y: string): string {
400
+ return `${x}\u0000${y}`;
401
+ }
402
+
403
+ function numberOf(value: string): number {
404
+ return Number(value.trim().replace(/%$/, ''));
405
+ }
package/src/index.ts CHANGED
@@ -73,10 +73,56 @@ export type { CanvasPalette } from './trading-host.js';
73
73
  // equity is not here - that one shares the price axis and belongs to the chart engine.
74
74
  export { compressPnl, drawPnlCurve, pnlCurve } from './pnl-curve.js';
75
75
 
76
+ // The same curve at the size of a chart: one control for an equity panel, so a host does not
77
+ // hand-roll the arithmetic a sparkline already does.
78
+ export { EquityWidget } from './equity-widget.js';
79
+
80
+ export type { EquityDeps } from './equity-widget.js';
81
+
82
+ // The chart engine the two chart panels are built on, re-exported so a host can reach the same
83
+ // createChart these panels use rather than resolving a second copy of it.
84
+ export { AreaSeries, CrosshairMode, LineSeries, createChart } from './chart-engine.js';
85
+
86
+ export type { IChartApi, ISeriesApi } from './chart-engine.js';
87
+
88
+ // One metric over two parameters. It knows nothing about optimisation - a grid of {x, y, value}
89
+ // is the same object whatever produced it - which is what lets a backtest sweep and anything
90
+ // else that varies two things share a control.
91
+ export { OptimizationHeatmapWidget } from './optimization-heatmap-widget.js';
92
+
93
+ export type { OptimizationHeatmapDeps } from './optimization-heatmap-widget.js';
94
+
95
+ export { HeatDirections, heatScale, hitHeatmap, layoutHeatmap, tintOf, valueAt } from './heatmap-grid.js';
96
+
97
+ // The same measurements as a landscape, turned by hand. No library under it: the projection is
98
+ // arithmetic and the gestures are pointer events, so a mouse and a thumb take the same path.
99
+ export { SurfaceWidget } from './surface-widget.js';
100
+
101
+ export type { SurfaceData, SurfaceDeps } from './surface-widget.js';
102
+
103
+ export {
104
+ DEFAULT_VIEW, MAX_PITCH, MIN_PITCH, clampView, dragView, project, surfaceLayout, zoomView,
105
+ } from './surface-grid.js';
106
+
107
+ export type { SurfaceAxis, SurfaceBox, SurfaceInput, SurfaceLayout, SurfaceQuad, SurfaceView } from './surface-grid.js';
108
+
109
+ export type {
110
+ HeatBucket, HeatCell, HeatCellShape, HeatDirection, HeatGapShape, HeatLabel, HeatLayout,
111
+ HeatLayoutInput, HeatLegend, HeatLegendStep, HeatMark, HeatRect, HeatScale,
112
+ } from './heatmap-grid.js';
113
+
76
114
  export type { PnlBox, PnlCurve, PnlCurveContext, PnlCurveStyle, PnlPoint } from './pnl-curve.js';
77
115
 
78
116
  export { OptionDeskWidget, greekPlaces, greekScales, scaleChain, sideGreeks } from './option-desk-widget.js';
79
117
 
118
+ // The smile: the same chain the desk reads, drawn as the shape a trader looks for rather than as
119
+ // a column of percentages.
120
+ export { OptionSmileWidget } from './option-smile-widget.js';
121
+
122
+ export type { OptionSmileDeps } from './option-smile-widget.js';
123
+
124
+ export { sideVolatility, sortedChain, toSmileSeries } from './option-smile-widget.js';
125
+
80
126
  export type { OptionChainContext, OptionDeskDeps, OptionSide, OptionStrike } from './option-desk-widget.js';
81
127
 
82
128
  // Option pricing, for a host that has quotes and an expiry but no pricing service. A host