@slickquant/slick-ladder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/canvas-renderer.d.ts +182 -0
- package/dist/canvas-renderer.d.ts.map +1 -0
- package/dist/interaction-handler.d.ts +35 -0
- package/dist/interaction-handler.d.ts.map +1 -0
- package/dist/main.d.ts +122 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/mbo-manager.d.ts +79 -0
- package/dist/mbo-manager.d.ts.map +1 -0
- package/dist/slick-ladder.js +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/wasm/SlickLadder.Core.wasm +0 -0
- package/dist/wasm/System.Collections.wasm +0 -0
- package/dist/wasm/System.Console.wasm +0 -0
- package/dist/wasm/System.Linq.wasm +0 -0
- package/dist/wasm/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/blazor.boot.json +33 -0
- package/dist/wasm/dotnet.js +4 -0
- package/dist/wasm/dotnet.js.map +1 -0
- package/dist/wasm/dotnet.native.js +17 -0
- package/dist/wasm/dotnet.native.js.symbols +3509 -0
- package/dist/wasm/dotnet.native.wasm +0 -0
- package/dist/wasm/dotnet.runtime.js +4 -0
- package/dist/wasm/dotnet.runtime.js.map +1 -0
- package/dist/wasm/supportFiles/0_runtimeconfig.bin +1 -0
- package/dist/wasm-adapter.d.ts +79 -0
- package/dist/wasm-adapter.d.ts.map +1 -0
- package/dist/wasm-types.d.ts +115 -0
- package/dist/wasm-types.d.ts.map +1 -0
- package/dist/wasm-worker-module.d.ts +6 -0
- package/dist/wasm-worker-module.d.ts.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @slickquant/slick-ladder
|
|
2
|
+
|
|
3
|
+
Ultra-low latency price ladder component for web applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @slickquant/slick-ladder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { PriceLadder } from '@slickquant/slick-ladder';
|
|
15
|
+
|
|
16
|
+
// Create a canvas element
|
|
17
|
+
const canvas = document.getElementById('ladder-canvas') as HTMLCanvasElement;
|
|
18
|
+
|
|
19
|
+
// Initialize the ladder
|
|
20
|
+
const ladder = new PriceLadder(canvas, {
|
|
21
|
+
// Configuration options
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Update market data
|
|
25
|
+
ladder.updateOrderBook({
|
|
26
|
+
bids: [[100.5, 1000], [100.4, 2000]],
|
|
27
|
+
asks: [[100.6, 1500], [100.7, 1800]]
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Ultra-low latency** rendering using HTML Canvas
|
|
34
|
+
- **WebAssembly powered** order book processing (optional)
|
|
35
|
+
- **Real-time updates** with minimal CPU overhead
|
|
36
|
+
- **Customizable appearance** and behavior
|
|
37
|
+
- **Interactive** - click to trade, drag to place orders
|
|
38
|
+
|
|
39
|
+
## WebAssembly Mode
|
|
40
|
+
|
|
41
|
+
The package includes pre-built WASM files for enhanced performance:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { initWasm } from '@slickquant/slick-ladder/wasm-adapter';
|
|
45
|
+
|
|
46
|
+
// Initialize WASM runtime
|
|
47
|
+
await initWasm('/path/to/wasm/files/');
|
|
48
|
+
|
|
49
|
+
// Now create ladder with WASM support
|
|
50
|
+
const ladder = new PriceLadder(canvas, {
|
|
51
|
+
useWasm: true
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## API Documentation
|
|
56
|
+
|
|
57
|
+
See the [main repository](https://github.com/SlickQuant/slick-ladder) for detailed documentation.
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Side, OrderBookSnapshot, CanvasColors, RenderMetrics } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Ultra-fast Canvas 2D renderer optimized for <1ms rendering of dirty regions.
|
|
4
|
+
* Uses dirty region tracking to minimize redraw operations.
|
|
5
|
+
*/
|
|
6
|
+
export declare class CanvasRenderer {
|
|
7
|
+
private canvas;
|
|
8
|
+
private ctx;
|
|
9
|
+
private offscreenCanvas;
|
|
10
|
+
private offscreenCtx;
|
|
11
|
+
private width;
|
|
12
|
+
private height;
|
|
13
|
+
private rowHeight;
|
|
14
|
+
private visibleRows;
|
|
15
|
+
private colors;
|
|
16
|
+
private dirtyRows;
|
|
17
|
+
private minDirtyRow;
|
|
18
|
+
private maxDirtyRow;
|
|
19
|
+
private needsFullRedraw;
|
|
20
|
+
private lastRemovalMode;
|
|
21
|
+
private lastShowOrderCount;
|
|
22
|
+
private lastShowVolumeBars;
|
|
23
|
+
private lastTickSize;
|
|
24
|
+
private lastDensePackingScrollOffset;
|
|
25
|
+
private lastReferencePrice;
|
|
26
|
+
private lastCenterPrice;
|
|
27
|
+
private showVolumeBars;
|
|
28
|
+
private showOrderCount;
|
|
29
|
+
private lastFrameTime;
|
|
30
|
+
private frameCount;
|
|
31
|
+
private fps;
|
|
32
|
+
private currentSnapshot;
|
|
33
|
+
private bidOrdersByPriceKey;
|
|
34
|
+
private askOrdersByPriceKey;
|
|
35
|
+
private scrollOffset;
|
|
36
|
+
private centerPrice;
|
|
37
|
+
private removalMode;
|
|
38
|
+
private tickSize;
|
|
39
|
+
constructor(canvas: HTMLCanvasElement, width: number, height: number, rowHeight?: number, colors?: CanvasColors, showVolumeBars?: boolean, showOrderCount?: boolean, tickSize?: number);
|
|
40
|
+
private setupContext;
|
|
41
|
+
/**
|
|
42
|
+
* Format price based on tick size
|
|
43
|
+
*/
|
|
44
|
+
private formatPrice;
|
|
45
|
+
private buildOrderLookup;
|
|
46
|
+
private drawFullBackground;
|
|
47
|
+
private renderBackground;
|
|
48
|
+
/**
|
|
49
|
+
* Render a price ladder snapshot with dirty region optimization
|
|
50
|
+
*/
|
|
51
|
+
render(snapshot: OrderBookSnapshot): void;
|
|
52
|
+
private renderFull;
|
|
53
|
+
private resolveReferencePrice;
|
|
54
|
+
private shouldFullRedraw;
|
|
55
|
+
private updateLastState;
|
|
56
|
+
private markRowDirty;
|
|
57
|
+
private renderDirty;
|
|
58
|
+
private buildLevelMap;
|
|
59
|
+
private renderDensePacking;
|
|
60
|
+
private buildDensePackingLayout;
|
|
61
|
+
private getDenseRowIndexForChange;
|
|
62
|
+
private tryGetDenseLevelForRow;
|
|
63
|
+
private priceToRowIndex;
|
|
64
|
+
private indexOfPrice;
|
|
65
|
+
private lowerBoundPrice;
|
|
66
|
+
private renderShowEmpty;
|
|
67
|
+
private renderRow;
|
|
68
|
+
private drawRowBackground;
|
|
69
|
+
private drawRowGridLines;
|
|
70
|
+
/**
|
|
71
|
+
* Render only the price label for a row (used in Show Empty mode)
|
|
72
|
+
*/
|
|
73
|
+
private renderPriceOnly;
|
|
74
|
+
/**
|
|
75
|
+
* Render data overlay (quantity, orders, bars) for a row that has a level
|
|
76
|
+
*/
|
|
77
|
+
private renderDataOverlay;
|
|
78
|
+
/**
|
|
79
|
+
* Draw individual order bars (MBO mode)
|
|
80
|
+
* Draws bars as horizontally adjacent segments within the row
|
|
81
|
+
*/
|
|
82
|
+
private drawIndividualOrders;
|
|
83
|
+
/**
|
|
84
|
+
* Format quantity for display in segment (e.g., "500", "1K", "2M")
|
|
85
|
+
*/
|
|
86
|
+
private formatQuantity;
|
|
87
|
+
private calculateMaxQuantity;
|
|
88
|
+
private copyToMainCanvas;
|
|
89
|
+
private markAllDirty;
|
|
90
|
+
private clearDirtyState;
|
|
91
|
+
private updateFPS;
|
|
92
|
+
private getOrdersForLevel;
|
|
93
|
+
/**
|
|
94
|
+
* Get performance metrics
|
|
95
|
+
*/
|
|
96
|
+
getMetrics(): RenderMetrics;
|
|
97
|
+
/**
|
|
98
|
+
* Convert screen X coordinate to column index
|
|
99
|
+
*/
|
|
100
|
+
screenXToColumn(x: number): number;
|
|
101
|
+
/**
|
|
102
|
+
* Get the price column index in the RENDERED layout
|
|
103
|
+
*/
|
|
104
|
+
getPriceColumn(): number;
|
|
105
|
+
/**
|
|
106
|
+
* Get the BID quantity column index
|
|
107
|
+
*/
|
|
108
|
+
getBidQtyColumn(): number;
|
|
109
|
+
/**
|
|
110
|
+
* Get the ASK quantity column index
|
|
111
|
+
*/
|
|
112
|
+
getAskQtyColumn(): number;
|
|
113
|
+
/**
|
|
114
|
+
* Convert screen Y coordinate to row index
|
|
115
|
+
*/
|
|
116
|
+
screenYToRow(y: number): number;
|
|
117
|
+
/**
|
|
118
|
+
* Convert row index to price
|
|
119
|
+
*/
|
|
120
|
+
rowToPrice(rowIndex: number): number | null;
|
|
121
|
+
/**
|
|
122
|
+
* Convert row index to level info (price and quantity)
|
|
123
|
+
*/
|
|
124
|
+
rowToLevelInfo(rowIndex: number): {
|
|
125
|
+
price: number;
|
|
126
|
+
quantity: number;
|
|
127
|
+
side: Side;
|
|
128
|
+
} | null;
|
|
129
|
+
/**
|
|
130
|
+
* Resize the canvas
|
|
131
|
+
*/
|
|
132
|
+
resize(width: number, height: number): void;
|
|
133
|
+
/**
|
|
134
|
+
* Set whether to show volume bars
|
|
135
|
+
*/
|
|
136
|
+
setShowVolumeBars(show: boolean): void;
|
|
137
|
+
/**
|
|
138
|
+
* Set whether to show order count
|
|
139
|
+
*/
|
|
140
|
+
setShowOrderCount(show: boolean): void;
|
|
141
|
+
/**
|
|
142
|
+
* Set scroll offset for dense packing mode
|
|
143
|
+
*/
|
|
144
|
+
setScrollOffset(offset: number): void;
|
|
145
|
+
/**
|
|
146
|
+
* Get current scroll offset
|
|
147
|
+
*/
|
|
148
|
+
getScrollOffset(): number;
|
|
149
|
+
/**
|
|
150
|
+
* Set center price for show empty mode (price-based scrolling)
|
|
151
|
+
*/
|
|
152
|
+
setCenterPrice(price: number): void;
|
|
153
|
+
/**
|
|
154
|
+
* Get current center price
|
|
155
|
+
*/
|
|
156
|
+
getCenterPrice(): number;
|
|
157
|
+
/**
|
|
158
|
+
* Reset center price (called when order book is cleared)
|
|
159
|
+
*/
|
|
160
|
+
resetCenterPrice(): void;
|
|
161
|
+
/**
|
|
162
|
+
* Scroll by price delta (for show empty mode)
|
|
163
|
+
*/
|
|
164
|
+
scrollByPrice(delta: number): void;
|
|
165
|
+
/**
|
|
166
|
+
* Set removal mode (affects how empty levels are handled)
|
|
167
|
+
*/
|
|
168
|
+
setRemovalMode(mode: 'showEmpty' | 'removeRow'): void;
|
|
169
|
+
/**
|
|
170
|
+
* Get current removal mode
|
|
171
|
+
*/
|
|
172
|
+
getRemovalMode(): 'showEmpty' | 'removeRow';
|
|
173
|
+
/**
|
|
174
|
+
* Get configured tick size
|
|
175
|
+
*/
|
|
176
|
+
getTickSize(): number;
|
|
177
|
+
/**
|
|
178
|
+
* Calculate canvas width based on enabled features
|
|
179
|
+
*/
|
|
180
|
+
private calculateCanvasWidth;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=canvas-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-renderer.d.ts","sourceRoot":"","sources":["../src/canvas-renderer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Side } from './types';
|
|
2
|
+
import { CanvasRenderer } from './canvas-renderer';
|
|
3
|
+
/**
|
|
4
|
+
* Handles user interactions with the price ladder (clicks, hover, scroll)
|
|
5
|
+
*/
|
|
6
|
+
export declare class InteractionHandler {
|
|
7
|
+
private canvas;
|
|
8
|
+
private renderer;
|
|
9
|
+
private readOnly;
|
|
10
|
+
private hoveredRow;
|
|
11
|
+
private hoveredPrice;
|
|
12
|
+
onPriceClick?: (price: number, side: Side) => void;
|
|
13
|
+
onPriceHover?: (price: number | null) => void;
|
|
14
|
+
onScroll?: (delta: number) => void;
|
|
15
|
+
constructor(canvas: HTMLCanvasElement, renderer: CanvasRenderer, readOnly?: boolean);
|
|
16
|
+
private setupEventListeners;
|
|
17
|
+
private handleClick;
|
|
18
|
+
private handleMouseMove;
|
|
19
|
+
private handleMouseLeave;
|
|
20
|
+
private handleWheel;
|
|
21
|
+
private handleContextMenu;
|
|
22
|
+
/**
|
|
23
|
+
* Update renderer reference
|
|
24
|
+
*/
|
|
25
|
+
setRenderer(renderer: CanvasRenderer): void;
|
|
26
|
+
/**
|
|
27
|
+
* Update read-only state
|
|
28
|
+
*/
|
|
29
|
+
setReadOnly(readOnly: boolean): void;
|
|
30
|
+
/**
|
|
31
|
+
* Clean up event listeners
|
|
32
|
+
*/
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=interaction-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction-handler.d.ts","sourceRoot":"","sources":["../src/interaction-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;GAEG;AACH,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAU;IAE1B,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,YAAY,CAAuB;IAGpC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;gBAE9B,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,GAAE,OAAe;IAQ1F,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,WAAW;IAkCnB,OAAO,CAAC,eAAe;IAwBvB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,iBAAiB;IAKzB;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAIlD;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAI3C;;OAEG;IACI,OAAO,IAAI,IAAI;CAOzB"}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { PriceLadderConfig, PriceLevel, OrderUpdate, OrderUpdateType } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Main Price Ladder component for web.
|
|
4
|
+
* Currently uses pure TypeScript implementation.
|
|
5
|
+
* Will be upgraded to use WASM core when available.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PriceLadder {
|
|
8
|
+
private container;
|
|
9
|
+
private canvas;
|
|
10
|
+
private renderer;
|
|
11
|
+
private interactionHandler;
|
|
12
|
+
private config;
|
|
13
|
+
private dataMode;
|
|
14
|
+
private bids;
|
|
15
|
+
private asks;
|
|
16
|
+
private mboManager;
|
|
17
|
+
private updateCount;
|
|
18
|
+
private lastRenderTime;
|
|
19
|
+
private dirtyChanges;
|
|
20
|
+
private hasStructuralChange;
|
|
21
|
+
private rafId;
|
|
22
|
+
constructor(config: PriceLadderConfig);
|
|
23
|
+
private setupInteractions;
|
|
24
|
+
/**
|
|
25
|
+
* Process a price level update
|
|
26
|
+
*/
|
|
27
|
+
processUpdate(update: PriceLevel): void;
|
|
28
|
+
/**
|
|
29
|
+
* Process multiple updates in batch
|
|
30
|
+
*/
|
|
31
|
+
processBatch(updates: PriceLevel[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* Process binary update (simulated - will use WASM)
|
|
34
|
+
*/
|
|
35
|
+
processUpdateBinary(_data: Uint8Array): void;
|
|
36
|
+
/**
|
|
37
|
+
* Process a single order update (MBO mode)
|
|
38
|
+
*/
|
|
39
|
+
processOrderUpdate(update: OrderUpdate, type: OrderUpdateType): void;
|
|
40
|
+
/**
|
|
41
|
+
* Process multiple order updates in batch (MBO mode)
|
|
42
|
+
*/
|
|
43
|
+
processOrderBatch(updates: Array<{
|
|
44
|
+
update: OrderUpdate;
|
|
45
|
+
type: OrderUpdateType;
|
|
46
|
+
}>): void;
|
|
47
|
+
/**
|
|
48
|
+
* Get current snapshot
|
|
49
|
+
*/
|
|
50
|
+
private getSnapshot;
|
|
51
|
+
/**
|
|
52
|
+
* Render loop (60 FPS)
|
|
53
|
+
*/
|
|
54
|
+
private startRenderLoop;
|
|
55
|
+
/**
|
|
56
|
+
* Get best bid
|
|
57
|
+
*/
|
|
58
|
+
getBestBid(): number | null;
|
|
59
|
+
/**
|
|
60
|
+
* Get best ask
|
|
61
|
+
*/
|
|
62
|
+
getBestAsk(): number | null;
|
|
63
|
+
/**
|
|
64
|
+
* Get mid price
|
|
65
|
+
*/
|
|
66
|
+
getMidPrice(): number | null;
|
|
67
|
+
/**
|
|
68
|
+
* Get spread
|
|
69
|
+
*/
|
|
70
|
+
getSpread(): number | null;
|
|
71
|
+
/**
|
|
72
|
+
* Get render metrics
|
|
73
|
+
*/
|
|
74
|
+
getMetrics(): {
|
|
75
|
+
updateCount: number;
|
|
76
|
+
bidLevels: number;
|
|
77
|
+
askLevels: number;
|
|
78
|
+
fps: number;
|
|
79
|
+
frameTime: number;
|
|
80
|
+
dirtyRowCount: number;
|
|
81
|
+
totalRows: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Resize the ladder
|
|
85
|
+
*/
|
|
86
|
+
resize(width?: number, height?: number): void;
|
|
87
|
+
/**
|
|
88
|
+
* Set read-only mode
|
|
89
|
+
*/
|
|
90
|
+
setReadOnly(readOnly: boolean): void;
|
|
91
|
+
/**
|
|
92
|
+
* Calculate width based on visible columns
|
|
93
|
+
* Base 6 columns: [bid_orders][bid_qty][price][ask_qty][ask_orders][bars]
|
|
94
|
+
* Remove columns when features are disabled
|
|
95
|
+
*/
|
|
96
|
+
private calculateWidth;
|
|
97
|
+
/**
|
|
98
|
+
* Set volume bars visibility
|
|
99
|
+
*/
|
|
100
|
+
setShowVolumeBars(show: boolean): void;
|
|
101
|
+
/**
|
|
102
|
+
* Set order count visibility
|
|
103
|
+
*/
|
|
104
|
+
setShowOrderCount(show: boolean): void;
|
|
105
|
+
/**
|
|
106
|
+
* Set data mode (PriceLevel or MBO)
|
|
107
|
+
*/
|
|
108
|
+
setDataMode(mode: 'PriceLevel' | 'MBO'): void;
|
|
109
|
+
/**
|
|
110
|
+
* Clear all data
|
|
111
|
+
*/
|
|
112
|
+
clear(): void;
|
|
113
|
+
private roundToTick;
|
|
114
|
+
/**
|
|
115
|
+
* Destroy the ladder and clean up resources
|
|
116
|
+
*/
|
|
117
|
+
destroy(): void;
|
|
118
|
+
}
|
|
119
|
+
export * from './types';
|
|
120
|
+
export { CanvasRenderer } from './canvas-renderer';
|
|
121
|
+
export { InteractionHandler } from './interaction-handler';
|
|
122
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,iBAAiB,EAEjB,UAAU,EAGV,WAAW,EACX,eAAe,EAKlB,MAAM,SAAS,CAAC;AAGjB;;;;GAIG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,QAAQ,CAAuB;IAGvC,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,mBAAmB,CAAkB;IAG7C,OAAO,CAAC,KAAK,CAAa;gBAEd,MAAM,EAAE,iBAAiB;IAwDrC,OAAO,CAAC,iBAAiB;IAezB;;OAEG;IACI,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IA2C9C;;OAEG;IACI,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI;IAUhD;;OAEG;IACI,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAMnD;;OAEG;IACI,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,GAAG,IAAI;IAa3E;;OAEG;IACI,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,eAAe,CAAA;KAAE,CAAC,GAAG,IAAI;IAe9F;;OAEG;IACH,OAAO,CAAC,WAAW;IAuDnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAevB;;OAEG;IACI,UAAU,IAAI,MAAM,GAAG,IAAI;IAWlC;;OAEG;IACI,UAAU,IAAI,MAAM,GAAG,IAAI;IAWlC;;OAEG;IACI,WAAW,IAAI,MAAM,GAAG,IAAI;IAMnC;;OAEG;IACI,SAAS,IAAI,MAAM,GAAG,IAAI;IAMjC;;OAEG;IACI,UAAU;;;;;;;;;IAkBjB;;OAEG;IACI,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAOpD;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAK3C;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAMtB;;OAEG;IACI,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IA0B7C;;OAEG;IACI,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IA0B7C;;OAEG;IACI,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,KAAK,GAAG,IAAI;IAapD;;OAEG;IACI,KAAK,IAAI,IAAI;IAWpB,OAAO,CAAC,WAAW;IAKnB;;OAEG;IACI,OAAO,IAAI,IAAI;CAKzB;AAOD,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Order, OrderUpdate, OrderUpdateType, BookLevel, OrderBookSnapshot, DirtyLevelChange } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Market-By-Order (MBO) Manager
|
|
4
|
+
*
|
|
5
|
+
* Tracks individual orders at each price level and aggregates to BookLevel for rendering.
|
|
6
|
+
* Provides O(1) OrderId lookup for fast Modify/Delete operations.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - OrderLevel per price: Maintains individual orders with cached aggregates
|
|
10
|
+
* - OrderIndex: Fast OrderId → (Price, Side) lookup
|
|
11
|
+
* - Aggregation: Automatically updates BookLevel on each order change
|
|
12
|
+
*/
|
|
13
|
+
export declare class MBOManager {
|
|
14
|
+
private bidLevels;
|
|
15
|
+
private askLevels;
|
|
16
|
+
private orderIndex;
|
|
17
|
+
private dirtyChanges;
|
|
18
|
+
private hasStructuralChange;
|
|
19
|
+
constructor();
|
|
20
|
+
/**
|
|
21
|
+
* Process an order add operation.
|
|
22
|
+
* Creates new OrderLevel if price doesn't exist, adds order to level, updates aggregate.
|
|
23
|
+
*/
|
|
24
|
+
processOrderAdd(update: OrderUpdate): BookLevel;
|
|
25
|
+
/**
|
|
26
|
+
* Process an order modify operation.
|
|
27
|
+
* Updates quantity of existing order, recalculates aggregate.
|
|
28
|
+
*/
|
|
29
|
+
processOrderModify(update: OrderUpdate): BookLevel | null;
|
|
30
|
+
/**
|
|
31
|
+
* Process an order delete operation.
|
|
32
|
+
* Removes order from level, removes level if empty.
|
|
33
|
+
*/
|
|
34
|
+
processOrderDelete(update: OrderUpdate): BookLevel | null;
|
|
35
|
+
/**
|
|
36
|
+
* Process OrderUpdate with specified type.
|
|
37
|
+
*/
|
|
38
|
+
processOrderUpdate(update: OrderUpdate, type: OrderUpdateType): BookLevel | null;
|
|
39
|
+
/**
|
|
40
|
+
* Get individual orders for bid levels (for rendering).
|
|
41
|
+
* Returns a map of price → Order[]
|
|
42
|
+
*/
|
|
43
|
+
getBidOrders(): Map<number, Order[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Get individual orders for ask levels (for rendering).
|
|
46
|
+
* Returns a map of price → Order[]
|
|
47
|
+
*/
|
|
48
|
+
getAskOrders(): Map<number, Order[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Get aggregated BookLevels for bids (highest to lowest)
|
|
51
|
+
*/
|
|
52
|
+
getBidLevels(): BookLevel[];
|
|
53
|
+
/**
|
|
54
|
+
* Get aggregated BookLevels for asks (lowest to highest)
|
|
55
|
+
*/
|
|
56
|
+
getAskLevels(): BookLevel[];
|
|
57
|
+
/**
|
|
58
|
+
* Get current order book snapshot
|
|
59
|
+
*/
|
|
60
|
+
getSnapshot(): OrderBookSnapshot;
|
|
61
|
+
/**
|
|
62
|
+
* Reset all state
|
|
63
|
+
*/
|
|
64
|
+
reset(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Get number of tracked orders
|
|
67
|
+
*/
|
|
68
|
+
getOrderCount(): number;
|
|
69
|
+
/**
|
|
70
|
+
* Get number of price levels
|
|
71
|
+
*/
|
|
72
|
+
getLevelCount(): number;
|
|
73
|
+
consumeDirtyState(): {
|
|
74
|
+
dirtyChanges: DirtyLevelChange[];
|
|
75
|
+
structuralChange: boolean;
|
|
76
|
+
};
|
|
77
|
+
private recordDirtyChange;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=mbo-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mbo-manager.d.ts","sourceRoot":"","sources":["../src/mbo-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA+BpH;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,UAAU,CAA6C;IAE/D,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,mBAAmB,CAAkB;;IAQ7C;;;OAGG;IACH,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS;IA2C/C;;;OAGG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI;IAiDzD;;;OAGG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI;IA0DzD;;OAEG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI;IAahF;;;OAGG;IACH,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;IAQpC;;;OAGG;IACH,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;IAQpC;;OAEG;IACH,YAAY,IAAI,SAAS,EAAE;IAgB3B;;OAEG;IACH,YAAY,IAAI,SAAS,EAAE;IAgB3B;;OAEG;IACH,WAAW,IAAI,iBAAiB;IAwBhC;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,aAAa,IAAI,MAAM;IAIvB;;OAEG;IACH,aAAa,IAAI,MAAM;IAIvB,iBAAiB,IAAI;QAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC;QAAC,gBAAgB,EAAE,OAAO,CAAA;KAAE;IAQpF,OAAO,CAAC,iBAAiB;CAY5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SlickLadder=e():t.SlickLadder=e()}(self,()=>(()=>{"use strict";var t,e,s={d:(t,e)=>{for(var i in e)s.o(e,i)&&!s.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},i={};s.r(i),s.d(i,{COL_WIDTH:()=>r,CanvasRenderer:()=>c,DEFAULT_COLORS:()=>a,InteractionHandler:()=>l,MIN_ORDER_SEGMENT_WIDTH:()=>h,ORDER_SEGMENT_GAP:()=>o,OrderUpdateType:()=>e,PriceLadder:()=>f,Side:()=>t,VOLUME_BAR_WIDTH_MULTIPLIER:()=>n}),function(t){t[t.BID=0]="BID",t[t.ASK=1]="ASK"}(t||(t={})),function(t){t[t.Add=0]="Add",t[t.Modify=1]="Modify",t[t.Delete=2]="Delete"}(e||(e={}));const r=66.7,n=2.5,o=2,h=30,a={background:"#1e1e1e",bidQtyBackground:"#1a2f3a",askQtyBackground:"#3a1a1f",priceBackground:"#3a3a3a",bidBar:"#4caf50",askBar:"#f44336",text:"#e0e0e0",gridLine:"#444444",ownOrderBorder:"#ffd700",hoverBackground:"rgba(255, 255, 255, 0.1)"};class c{constructor(t,e,s,i=24,r=a,n=!0,o=!0,h=.01){this.minDirtyRow=1/0,this.maxDirtyRow=-1,this.needsFullRedraw=!0,this.lastRemovalMode="removeRow",this.lastShowOrderCount=!0,this.lastShowVolumeBars=!0,this.lastTickSize=.01,this.lastDensePackingScrollOffset=0,this.lastReferencePrice=0,this.lastCenterPrice=0,this.showVolumeBars=!0,this.showOrderCount=!0,this.lastFrameTime=0,this.frameCount=0,this.fps=60,this.currentSnapshot=null,this.bidOrdersByPriceKey=null,this.askOrdersByPriceKey=null,this.scrollOffset=0,this.centerPrice=0,this.removalMode="removeRow",this.canvas=t,this.width=e,this.height=s,this.rowHeight=i,this.colors=r,this.showVolumeBars=n,this.showOrderCount=o,this.tickSize=h,this.visibleRows=Math.floor(s/i),t.width=e,t.height=s,t.style.width=`${e}px`,t.style.height=`${s}px`;const c=t.getContext("2d",{alpha:!1});if(!c)throw new Error("Failed to get 2D context");this.ctx=c,this.offscreenCanvas=new OffscreenCanvas(e,s);const l=this.offscreenCanvas.getContext("2d",{alpha:!1});if(!l)throw new Error("Failed to get offscreen 2D context");this.offscreenCtx=l,this.dirtyRows=new Set,this.setupContext(this.ctx),this.setupContext(this.offscreenCtx),this.renderBackground()}setupContext(t){t.font="14px monospace",t.textBaseline="middle",t.imageSmoothingEnabled=!1}formatPrice(t){let e=0,s=this.tickSize;for(;s<1&&e<10;)s*=10,e++;for(;e<10;){const t=Math.pow(10,e),s=Math.round(this.tickSize*t);if(Math.abs(s/t-this.tickSize)<1e-10)break;e++}return t.toFixed(e)}buildOrderLookup(t){if(!t)return null;const e=new Map;for(const[s,i]of t.entries())e.set(this.formatPrice(s),i);return e}drawFullBackground(){this.offscreenCtx.fillStyle=this.colors.background,this.offscreenCtx.fillRect(0,0,this.width,this.height);const t=this.showOrderCount?1:0,e=this.showOrderCount?2:1,s=this.showOrderCount?3:2;this.offscreenCtx.fillStyle=this.colors.bidQtyBackground,this.offscreenCtx.fillRect(r*t,0,r,this.height),this.offscreenCtx.fillStyle=this.colors.priceBackground,this.offscreenCtx.fillRect(r*e,0,r,this.height),this.offscreenCtx.fillStyle=this.colors.askQtyBackground,this.offscreenCtx.fillRect(r*s,0,r,this.height),this.offscreenCtx.strokeStyle=this.colors.gridLine,this.offscreenCtx.lineWidth=1;for(let t=0;t<=this.visibleRows;t++){const e=t*this.rowHeight;this.offscreenCtx.beginPath(),this.offscreenCtx.moveTo(0,e),this.offscreenCtx.lineTo(this.width,e),this.offscreenCtx.stroke()}}renderBackground(){this.drawFullBackground(),this.copyToMainCanvas(),this.needsFullRedraw=!0}render(t){const e=performance.now();this.currentSnapshot=t,this.bidOrdersByPriceKey=this.buildOrderLookup(t.bidOrders),this.askOrdersByPriceKey=this.buildOrderLookup(t.askOrders),this.clearDirtyState();const s="showEmpty"===this.removalMode?this.resolveReferencePrice(t):0;this.shouldFullRedraw(t,s)?(this.renderFull(t,s),this.markAllDirty()):this.renderDirty(t,s),this.updateLastState(s);const i=performance.now()-e;this.updateFPS(i)}renderFull(t,e){if(this.drawFullBackground(),"removeRow"===this.removalMode)this.renderDensePacking(t);else{const s=this.buildLevelMap(t);this.renderShowEmpty(t,e,s)}this.copyToMainCanvas()}resolveReferencePrice(t){let e=this.centerPrice;if(0===t.bids.length&&0===t.asks.length)this.centerPrice=0,e=0;else if(0===this.centerPrice){const s=t.midPrice??100;this.centerPrice=Math.round(s/this.tickSize)*this.tickSize,e=this.centerPrice}else e=this.centerPrice;return e}shouldFullRedraw(t,e){if(this.needsFullRedraw||!t.dirtyChanges)return!0;if(0===t.bids.length&&0===t.asks.length)return!0;if(this.lastRemovalMode!==this.removalMode||this.lastShowOrderCount!==this.showOrderCount||this.lastShowVolumeBars!==this.showVolumeBars||this.lastTickSize!==this.tickSize)return!0;if("removeRow"===this.removalMode){if(this.lastDensePackingScrollOffset!==this.scrollOffset)return!0}else if(this.lastReferencePrice!==e||this.lastCenterPrice!==this.centerPrice)return!0;return!1}updateLastState(t){this.needsFullRedraw=!1,this.lastRemovalMode=this.removalMode,this.lastShowOrderCount=this.showOrderCount,this.lastShowVolumeBars=this.showVolumeBars,this.lastTickSize=this.tickSize,this.lastDensePackingScrollOffset=this.scrollOffset,this.lastReferencePrice=t,this.lastCenterPrice=this.centerPrice}markRowDirty(t){t<0||t>this.visibleRows||(this.dirtyRows.add(t),t<this.minDirtyRow&&(this.minDirtyRow=t),t>this.maxDirtyRow&&(this.maxDirtyRow=t))}renderDirty(e,s){if(!e.dirtyChanges||0===e.dirtyChanges.length)return;const i=Math.floor(this.visibleRows/2);let r=null,n=null;"removeRow"===this.removalMode?r=this.buildDensePackingLayout(e):n=this.buildLevelMap(e);for(const t of e.dirtyChanges){let e=null;"removeRow"===this.removalMode?r&&(e=this.getDenseRowIndexForChange(t,r)):e=this.priceToRowIndex(t.price,s,i),null!==e&&this.markRowDirty(e)}if("removeRow"===this.removalMode&&e.structuralChange&&r){let t=this.visibleRows,i=!1;for(const s of e.dirtyChanges){if(!s.isRemoval&&!s.isAddition)continue;const e=this.getDenseRowIndexForChange(s,r);null!==e&&(t=Math.min(t,e),i=!0)}if(!i)return this.renderFull(e,s),void this.markAllDirty();for(let e=t;e<=this.visibleRows;e++)this.markRowDirty(e)}if(0!==this.dirtyRows.size){for(const o of this.dirtyRows){const h=o*this.rowHeight;if(!(h<0||h>=this.height))if(this.drawRowBackground(o),this.drawRowGridLines(o),"removeRow"===this.removalMode){if(!r)continue;const s=this.tryGetDenseLevelForRow(o,r);if(s){const i=Math.round(s.level.price/this.tickSize)*this.tickSize,r=s.side===t.ASK?this.getOrdersForLevel(e.askOrders,i,this.askOrdersByPriceKey):this.getOrdersForLevel(e.bidOrders,i,this.bidOrdersByPriceKey);this.renderRow(o,s.level,r)}}else{const r=s-(o-i)*this.tickSize,h=Math.round(r/this.tickSize)*this.tickSize,a=this.formatPrice(h);this.renderPriceOnly(o,h);const c=n?.get(a);if(c){const s=c.side===t.BID?e.bidOrders:e.askOrders,i=this.getOrdersForLevel(s,h,c.side===t.BID?this.bidOrdersByPriceKey:this.askOrdersByPriceKey);this.renderDataOverlay(o,c,i)}}}for(const t of this.dirtyRows){const e=t*this.rowHeight;e<0||e>=this.height||this.ctx.drawImage(this.offscreenCanvas,0,e,this.width,this.rowHeight,0,e,this.width,this.rowHeight)}}}buildLevelMap(t){const e=new Map;for(const s of t.asks)if(s.quantity>0){const t=Math.round(s.price/this.tickSize)*this.tickSize,i=this.formatPrice(t);e.set(i,s)}for(const s of t.bids)if(s.quantity>0){const t=Math.round(s.price/this.tickSize)*this.tickSize,i=this.formatPrice(t);e.set(i,s)}return e}renderDensePacking(t){const e=this.buildDensePackingLayout(t);let s=e.topOffset;for(let i=0;i<e.askRowsToRender;i++){const r=e.nonEmptyAsks.length-1-e.startAskIndex-i;if(r>=0&&r<e.nonEmptyAsks.length){const n=s+i*this.rowHeight;if(n>=0&&n<this.height){const s=e.nonEmptyAsks[r],i=Math.round(s.price/this.tickSize)*this.tickSize,o=this.getOrdersForLevel(t.askOrders,i,this.askOrdersByPriceKey);this.renderRow(Math.floor(n/this.rowHeight),s,o)}}}s=e.topOffset+e.askRowsToRender*this.rowHeight;for(let i=0;i<e.bidRowsToRender;i++){const r=e.nonEmptyBids.length-1-e.startBidIndex-i;if(r>=0&&r<e.nonEmptyBids.length){const n=s+i*this.rowHeight;if(n>=0&&n<this.height){const s=e.nonEmptyBids[r],i=Math.round(s.price/this.tickSize)*this.tickSize,o=this.getOrdersForLevel(t.bidOrders,i,this.bidOrdersByPriceKey);this.renderRow(Math.floor(n/this.rowHeight),s,o)}}}}buildDensePackingLayout(t){const e=t.asks.filter(t=>t.quantity>0),s=t.bids.filter(t=>t.quantity>0),i=Math.floor(this.visibleRows/2),r=e.length+s.length,n=e.length-i+this.scrollOffset;let o,h,a,c,l=0;if(n<0){l=-n*this.rowHeight,o=0,h=Math.min(e.length,Math.max(0,this.visibleRows+n)),a=0;const t=Math.max(0,this.visibleRows+n-e.length);c=Math.min(s.length,t)}else if(n<e.length){o=n,h=Math.min(e.length-o,this.visibleRows),a=0;const t=this.visibleRows-h;c=Math.min(s.length,t)}else if(n<r){o=e.length,h=0;a=n-e.length,c=Math.min(s.length-a,this.visibleRows)}else o=e.length,h=0,a=s.length,c=0;return{nonEmptyAsks:e,nonEmptyBids:s,startAskIndex:o,askRowsToRender:h,startBidIndex:a,bidRowsToRender:c,topOffset:l,firstRowIndex:Math.floor(l/this.rowHeight)}}getDenseRowIndexForChange(e,s){if(e.side===t.ASK){let t=this.indexOfPrice(s.nonEmptyAsks,e.price);t<0&&(t=this.lowerBoundPrice(s.nonEmptyAsks,e.price));const i=s.nonEmptyAsks.length-1-s.startAskIndex-t;return i>=0&&i<s.askRowsToRender?s.firstRowIndex+i:null}let i=this.indexOfPrice(s.nonEmptyBids,e.price);i<0&&(i=this.lowerBoundPrice(s.nonEmptyBids,e.price));const r=s.nonEmptyBids.length-1-s.startBidIndex-i;return r>=0&&r<s.bidRowsToRender?s.firstRowIndex+s.askRowsToRender+r:null}tryGetDenseLevelForRow(e,s){const i=e-s.firstRowIndex;if(i<0)return null;if(i<s.askRowsToRender){const e=s.nonEmptyAsks.length-1-s.startAskIndex-i;return e>=0&&e<s.nonEmptyAsks.length?{level:s.nonEmptyAsks[e],side:t.ASK}:null}const r=i-s.askRowsToRender;if(r<s.bidRowsToRender){const e=s.nonEmptyBids.length-1-s.startBidIndex-r;if(e>=0&&e<s.nonEmptyBids.length)return{level:s.nonEmptyBids[e],side:t.BID}}return null}priceToRowIndex(t,e,s){const i=t-e;return s+-Math.round(i/this.tickSize)}indexOfPrice(t,e){for(let s=0;s<t.length;s++)if(t[s].price===e)return s;return-1}lowerBoundPrice(t,e){let s=0,i=t.length;for(;s<i;){const r=Math.floor((s+i)/2);t[r].price<e?s=r+1:i=r}return s}renderShowEmpty(e,s,i){const r=Math.floor(this.visibleRows/2);for(let t=0;t<=this.visibleRows;t++){const e=s-(t-r)*this.tickSize,i=Math.round(e/this.tickSize)*this.tickSize;this.renderPriceOnly(t,i)}const n=i??this.buildLevelMap(e);for(let i=0;i<=this.visibleRows;i++){const o=s-(i-r)*this.tickSize,h=Math.round(o/this.tickSize)*this.tickSize,a=this.formatPrice(h),c=n.get(a);if(c){const s=c.side===t.BID?e.bidOrders:e.askOrders,r=this.getOrdersForLevel(s,h,c.side===t.BID?this.bidOrdersByPriceKey:this.askOrdersByPriceKey);this.renderDataOverlay(i,c,r)}}}renderRow(e,s,i){const o=e*this.rowHeight,h=this.formatPrice(s.price),a=s.quantity.toLocaleString(),c=`(${s.numOrders})`;let l=this.showOrderCount?0:-1,d=this.showOrderCount?1:0,u=this.showOrderCount?2:1,f=this.showOrderCount?3:2,g=this.showOrderCount?4:-1,w=3;this.showOrderCount&&(w+=2);let y=w;const p=this.showVolumeBars?this.calculateMaxQuantity():0,m=r*n-5,C=p>0?s.quantity/p*m:0;this.offscreenCtx.fillStyle=this.colors.text,this.offscreenCtx.textAlign="center",s.side===t.BID?(l>=0&&this.offscreenCtx.fillText(c,r*(l+.5),o+this.rowHeight/2),this.offscreenCtx.fillText(a,r*(d+.5),o+this.rowHeight/2),this.offscreenCtx.fillText(h,r*(u+.5),o+this.rowHeight/2),this.showVolumeBars&&(i&&i.length>0?this.drawIndividualOrders(i,t.BID,p,o):C>0&&(this.offscreenCtx.fillStyle=this.colors.bidBar,this.offscreenCtx.fillRect(r*y,o+4,C,this.rowHeight-8)))):(this.offscreenCtx.fillText(h,r*(u+.5),o+this.rowHeight/2),this.offscreenCtx.fillText(a,r*(f+.5),o+this.rowHeight/2),g>=0&&this.offscreenCtx.fillText(c,r*(g+.5),o+this.rowHeight/2),this.showVolumeBars&&(i&&i.length>0?this.drawIndividualOrders(i,t.ASK,p,o):C>0&&(this.offscreenCtx.fillStyle=this.colors.askBar,this.offscreenCtx.fillRect(r*y,o+4,C,this.rowHeight-8))))}drawRowBackground(t){const e=t*this.rowHeight;this.offscreenCtx.fillStyle=this.colors.background,this.offscreenCtx.fillRect(0,e,this.width,this.rowHeight);const s=this.showOrderCount?1:0,i=this.showOrderCount?2:1,n=this.showOrderCount?3:2;this.offscreenCtx.fillStyle=this.colors.bidQtyBackground,this.offscreenCtx.fillRect(r*s,e,r,this.rowHeight),this.offscreenCtx.fillStyle=this.colors.priceBackground,this.offscreenCtx.fillRect(r*i,e,r,this.rowHeight),this.offscreenCtx.fillStyle=this.colors.askQtyBackground,this.offscreenCtx.fillRect(r*n,e,r,this.rowHeight)}drawRowGridLines(t){const e=t*this.rowHeight,s=e+this.rowHeight;this.offscreenCtx.strokeStyle=this.colors.gridLine,this.offscreenCtx.lineWidth=1,this.offscreenCtx.beginPath(),this.offscreenCtx.moveTo(0,e),this.offscreenCtx.lineTo(this.width,e),this.offscreenCtx.stroke(),s<=this.height&&(this.offscreenCtx.beginPath(),this.offscreenCtx.moveTo(0,s),this.offscreenCtx.lineTo(this.width,s),this.offscreenCtx.stroke())}renderPriceOnly(t,e){const s=t*this.rowHeight,i=this.formatPrice(e),n=this.showOrderCount?2:1;this.offscreenCtx.fillStyle=this.colors.text,this.offscreenCtx.textAlign="center",this.offscreenCtx.fillText(i,r*(n+.5),s+this.rowHeight/2)}renderDataOverlay(e,s,i){const o=e*this.rowHeight,h=s.quantity.toLocaleString(),a=`(${s.numOrders})`,c=this.showOrderCount?0:-1,l=this.showOrderCount?1:0,d=this.showOrderCount?3:2,u=this.showOrderCount?4:-1;let f=3;this.showOrderCount&&(f+=2);const g=f,w=this.showVolumeBars?this.calculateMaxQuantity():0,y=r*n-5,p=w>0?s.quantity/w*y:0;this.offscreenCtx.fillStyle=this.colors.text,this.offscreenCtx.textAlign="center",s.side===t.BID?(c>=0&&this.offscreenCtx.fillText(a,r*(c+.5),o+this.rowHeight/2),this.offscreenCtx.fillText(h,r*(l+.5),o+this.rowHeight/2),this.showVolumeBars&&(i&&i.length>0?this.drawIndividualOrders(i,t.BID,w,o):p>0&&(this.offscreenCtx.fillStyle=this.colors.bidBar,this.offscreenCtx.fillRect(r*g,o+4,p,this.rowHeight-8)))):(this.offscreenCtx.fillText(h,r*(d+.5),o+this.rowHeight/2),u>=0&&this.offscreenCtx.fillText(a,r*(u+.5),o+this.rowHeight/2),this.showVolumeBars&&(i&&i.length>0?this.drawIndividualOrders(i,t.ASK,w,o):p>0&&(this.offscreenCtx.fillStyle=this.colors.askBar,this.offscreenCtx.fillRect(r*g,o+4,p,this.rowHeight-8))))}drawIndividualOrders(e,s,i,a){if(!e||0===e.length||!this.showVolumeBars)return;let c=3;this.showOrderCount&&(c+=2);const l=r*c,d=r*n-5,u=this.rowHeight-8,f=s===t.BID?this.colors.bidBar:this.colors.askBar,g=o,w=h;let y=0;for(const t of e)y+=t.quantity;if(y<=0)return;let p=i>0?y/i*d:0;const m=e.length*w+g*Math.max(0,e.length-1);if(p=Math.max(p,Math.min(m,d)),p<=0)return;const C=e.length>1?g:0,v=C*(e.length-1),k=Math.max(0,p-v),O=Math.min(w,k/e.length);let x=0,S=0;for(const t of e){const e=t.quantity/y*k;e<O?x+=O-e:e>O&&(S+=e-O)}const R=x>0&&S>0?x/S:0;let M=l;for(let t=0;t<e.length;t++){const s=e[t];let i=s.quantity/y*k;if(i<O?i=O:R>0&&(i-=(i-O)*R),i>0){this.offscreenCtx.fillStyle=f,this.offscreenCtx.fillRect(M,a+4,i,u);const t=this.formatQuantity(s.quantity);this.offscreenCtx.font="10px monospace";this.offscreenCtx.measureText(t).width<i-4&&(this.offscreenCtx.fillStyle=this.colors.text,this.offscreenCtx.textAlign="center",this.offscreenCtx.textBaseline="middle",this.offscreenCtx.fillText(t,M+i/2,a+4+u/2)),this.offscreenCtx.font="14px monospace",this.offscreenCtx.textAlign="left",this.offscreenCtx.textBaseline="middle",s.isOwnOrder&&(this.offscreenCtx.strokeStyle=this.colors.ownOrderBorder,this.offscreenCtx.lineWidth=2,this.offscreenCtx.strokeRect(M,a+4,i,u))}if(M+=i,C>0&&t<e.length-1&&(M+=C),M>=l+p)break}}formatQuantity(t){return t>=1e6?`${Math.floor(t/1e6)}M`:t>=1e3?`${Math.floor(t/1e3)}K`:t.toString()}calculateMaxQuantity(){if(!this.currentSnapshot)return 1;let t=0;for(const e of this.currentSnapshot.bids)t=Math.max(t,e.quantity);for(const e of this.currentSnapshot.asks)t=Math.max(t,e.quantity);return t||1}copyToMainCanvas(){this.ctx.drawImage(this.offscreenCanvas,0,0)}markAllDirty(){this.dirtyRows.clear(),this.minDirtyRow=0,this.maxDirtyRow=this.visibleRows-1}clearDirtyState(){this.dirtyRows.clear(),this.minDirtyRow=1/0,this.maxDirtyRow=-1}updateFPS(t){this.frameCount++;const e=performance.now();e-this.lastFrameTime>=1e3&&(this.fps=this.frameCount,this.frameCount=0,this.lastFrameTime=e)}getOrdersForLevel(t,e,s){if(!t)return;if(s){const t=s.get(this.formatPrice(e));if(t)return t}let i=t.get(e);if(i)return i;const r=Math.round(e/this.tickSize)*this.tickSize;if(i=t.get(r),i)return i;const n=Math.max(this.tickSize/1e3,1e-6);for(const[s,i]of t.entries())if(Math.abs(s-e)<=n)return i;const o=Math.round(r/this.tickSize);for(const[e,s]of t.entries()){if(Math.round(e/this.tickSize)===o)return s}const h=this.formatPrice(r);for(const[e,s]of t.entries())if(this.formatPrice(e)===h)return s}getMetrics(){const t=this.minDirtyRow===1/0?0:Math.max(0,this.maxDirtyRow-this.minDirtyRow+1);return{fps:this.fps,frameTime:1e3/this.fps,dirtyRowCount:t,totalRows:this.visibleRows}}screenXToColumn(t){return Math.floor(t/r)}getPriceColumn(){return this.showOrderCount?2:1}getBidQtyColumn(){return this.showOrderCount?1:0}getAskQtyColumn(){return this.showOrderCount?3:2}screenYToRow(t){return Math.floor(t/this.rowHeight)}rowToPrice(t){const e=this.rowToLevelInfo(t);return e?.price??null}rowToLevelInfo(e){if(!this.currentSnapshot)return null;if("removeRow"===this.removalMode){const s=this.buildDensePackingLayout(this.currentSnapshot),i=e-s.firstRowIndex;if(i<0)return null;if(i<s.askRowsToRender){const e=s.nonEmptyAsks.length-1-s.startAskIndex-i;if(e>=0&&e<s.nonEmptyAsks.length){const i=s.nonEmptyAsks[e];return{price:i.price,quantity:i.quantity,side:t.ASK}}return null}const r=i-s.askRowsToRender;if(r>=0&&r<s.bidRowsToRender){const e=s.nonEmptyBids.length-1-s.startBidIndex-r;if(e>=0&&e<s.nonEmptyBids.length){const i=s.nonEmptyBids[e];return{price:i.price,quantity:i.quantity,side:t.BID}}}return null}{const s=Math.floor(this.visibleRows/2),i=(0!==this.centerPrice?this.centerPrice:this.currentSnapshot.midPrice??5e4)-(e-s)*this.tickSize,r=Math.round(i/this.tickSize)*this.tickSize,n=this.currentSnapshot.asks.find(t=>Math.abs(t.price-r)<.5*this.tickSize);if(n&&n.quantity>0)return{price:n.price,quantity:n.quantity,side:t.ASK};const o=this.currentSnapshot.bids.find(t=>Math.abs(t.price-r)<.5*this.tickSize);return o&&o.quantity>0?{price:o.price,quantity:o.quantity,side:t.BID}:null}}resize(t,e){this.width=t,this.height=e,this.visibleRows=Math.floor(e/this.rowHeight),this.canvas.width=t,this.canvas.height=e,this.canvas.style.width=`${t}px`,this.canvas.style.height=`${e}px`,this.offscreenCanvas=new OffscreenCanvas(t,e);const s=this.offscreenCanvas.getContext("2d",{alpha:!1});s&&(this.offscreenCtx=s,this.setupContext(this.offscreenCtx)),this.renderBackground(),this.currentSnapshot&&this.render(this.currentSnapshot)}setShowVolumeBars(t){if(this.showVolumeBars!==t){this.showVolumeBars=t;const e=this.calculateCanvasWidth();this.resize(e,this.height)}}setShowOrderCount(t){if(this.showOrderCount!==t){this.showOrderCount=t;const e=this.calculateCanvasWidth();this.resize(e,this.height)}}setScrollOffset(t){this.scrollOffset=t,this.currentSnapshot&&this.render(this.currentSnapshot)}getScrollOffset(){return this.scrollOffset}setCenterPrice(t){this.centerPrice=t,this.currentSnapshot&&this.render(this.currentSnapshot)}getCenterPrice(){return this.centerPrice}resetCenterPrice(){this.centerPrice=0,this.needsFullRedraw=!0}scrollByPrice(t){if(0===this.centerPrice&&this.currentSnapshot){const t=this.currentSnapshot.midPrice;null!==t&&(this.centerPrice=Math.round(t/this.tickSize)*this.tickSize)}this.centerPrice=Math.round((this.centerPrice+t)/this.tickSize)*this.tickSize,this.currentSnapshot&&this.render(this.currentSnapshot)}setRemovalMode(t){this.removalMode=t,this.scrollOffset=0,this.centerPrice=0,this.needsFullRedraw=!0,this.currentSnapshot&&this.render(this.currentSnapshot)}getRemovalMode(){return this.removalMode}getTickSize(){return this.tickSize}calculateCanvasWidth(){const t=this.showOrderCount?5:3,e=this.showVolumeBars?r*n:0;return Math.round(t*r+e)}}class l{constructor(t,e,s=!1){this.hoveredRow=null,this.hoveredPrice=null,this.canvas=t,this.renderer=e,this.readOnly=s,this.setupEventListeners()}setupEventListeners(){this.canvas.addEventListener("click",this.handleClick.bind(this)),this.canvas.addEventListener("mousemove",this.handleMouseMove.bind(this)),this.canvas.addEventListener("mouseleave",this.handleMouseLeave.bind(this)),this.canvas.addEventListener("wheel",this.handleWheel.bind(this),{passive:!1}),this.canvas.addEventListener("contextmenu",this.handleContextMenu.bind(this))}handleClick(e){if(this.readOnly)return;const s=this.canvas.getBoundingClientRect(),i=e.clientX-s.left,r=e.clientY-s.top,n=this.renderer.screenYToRow(r),o=this.renderer.rowToLevelInfo(n);if(null!==o){const e=this.renderer.screenXToColumn(i),s=this.renderer.getBidQtyColumn(),r=this.renderer.getAskQtyColumn();e===s?(console.log("Action: BUY"),this.onPriceClick?.(o.price,t.ASK)):e===r?(console.log("Action: SELL"),this.onPriceClick?.(o.price,t.BID)):console.log("Action: none (clicked outside quantity columns)")}}handleMouseMove(t){const e=this.canvas.getBoundingClientRect(),s=t.clientY-e.top,i=this.renderer.screenYToRow(s);if(i!==this.hoveredRow){this.hoveredRow=i;const t=this.renderer.rowToPrice(i);t!==this.hoveredPrice&&(this.hoveredPrice=t,this.onPriceHover?.(t))}this.readOnly||null===this.hoveredPrice?this.canvas.style.cursor="default":this.canvas.style.cursor="pointer"}handleMouseLeave(){this.hoveredRow=null,this.hoveredPrice=null,this.onPriceHover?.(null),this.canvas.style.cursor="default"}handleWheel(t){t.preventDefault();const e=Math.sign(t.deltaY);if("removeRow"===this.renderer.getRemovalMode()){const t=5*e;this.onScroll?.(t)}else{const t=(e>0?-5:5)*this.renderer.getTickSize();this.renderer.scrollByPrice(t)}}handleContextMenu(t){t.preventDefault()}setRenderer(t){this.renderer=t}setReadOnly(t){this.readOnly=t}destroy(){this.canvas.removeEventListener("click",this.handleClick),this.canvas.removeEventListener("mousemove",this.handleMouseMove),this.canvas.removeEventListener("mouseleave",this.handleMouseLeave),this.canvas.removeEventListener("wheel",this.handleWheel),this.canvas.removeEventListener("contextmenu",this.handleContextMenu)}}class d{constructor(t,e){this.price=t,this.side=e,this.orders=new Map,this.totalQuantity=0,this.orderCount=0,this.isDirty=!0}getOrdersArray(){return Array.from(this.orders.values())}}class u{constructor(){this.dirtyChanges=[],this.hasStructuralChange=!1,this.bidLevels=new Map,this.askLevels=new Map,this.orderIndex=new Map}processOrderAdd(e){const s=e.price,i=e.side,r=i===t.BID?this.bidLevels:this.askLevels;let n=r.get(s);const o=!!n;n||(n=new d(s,i),r.set(s,n));const h={orderId:e.orderId,quantity:e.quantity,priority:e.priority,isOwnOrder:e.isOwnOrder??!1};return n.orders.set(e.orderId,h),n.totalQuantity+=e.quantity,n.orderCount++,n.isDirty=!0,this.orderIndex.set(e.orderId,{price:s,side:i}),this.recordDirtyChange(s,i,!1,!o),{price:s,quantity:n.totalQuantity,numOrders:n.orderCount,side:i,isDirty:!0,hasOwnOrders:!1}}processOrderModify(e){const s=this.orderIndex.get(e.orderId);if(!s)return null;const i=(s.side===t.BID?this.bidLevels:this.askLevels).get(s.price);if(!i)return this.orderIndex.delete(e.orderId),null;const r=i.orders.get(e.orderId);if(!r)return this.orderIndex.delete(e.orderId),null;const n=e.quantity-r.quantity;i.totalQuantity+=n,i.isDirty=!0;const o={orderId:e.orderId,quantity:e.quantity,priority:r.priority,isOwnOrder:r.isOwnOrder};return i.orders.set(e.orderId,o),{price:s.price,quantity:i.totalQuantity,numOrders:i.orderCount,side:s.side,isDirty:!0,hasOwnOrders:!1}}processOrderDelete(e){const s=this.orderIndex.get(e.orderId);if(!s)return null;this.orderIndex.delete(e.orderId);const i=s.side===t.BID?this.bidLevels:this.askLevels,r=i.get(s.price);if(!r)return null;const n=r.orders.get(e.orderId);if(!n)return null;r.totalQuantity-=n.quantity,r.orderCount--,r.orders.delete(e.orderId),r.isDirty=!0;return 0===r.orderCount?(i.delete(s.price),this.recordDirtyChange(s.price,s.side,!0,!1),{price:s.price,quantity:0,numOrders:0,side:s.side,isDirty:!0,hasOwnOrders:!1}):(this.recordDirtyChange(s.price,s.side,!1,!1),{price:s.price,quantity:r.totalQuantity,numOrders:r.orderCount,side:s.side,isDirty:!0,hasOwnOrders:!1})}processOrderUpdate(t,s){switch(s){case e.Add:return this.processOrderAdd(t);case e.Modify:return this.processOrderModify(t);case e.Delete:return this.processOrderDelete(t);default:return null}}getBidOrders(){const t=new Map;for(const[e,s]of this.bidLevels)t.set(e,s.getOrdersArray());return t}getAskOrders(){const t=new Map;for(const[e,s]of this.askLevels)t.set(e,s.getOrdersArray());return t}getBidLevels(){const e=[];for(const[s,i]of this.bidLevels)e.push({price:s,quantity:i.totalQuantity,numOrders:i.orderCount,side:t.BID,isDirty:i.isDirty,hasOwnOrders:!1});return e.sort((t,e)=>t.price-e.price)}getAskLevels(){const e=[];for(const[s,i]of this.askLevels)e.push({price:s,quantity:i.totalQuantity,numOrders:i.orderCount,side:t.ASK,isDirty:i.isDirty,hasOwnOrders:!1});return e.sort((t,e)=>t.price-e.price)}getSnapshot(){const t=this.getBidLevels(),e=this.getAskLevels(),s=t.length>0?t[0].price:null,i=e.length>0?e[0].price:null,r=null!==s&&null!==i?(s+i)/2:null,n=this.consumeDirtyState();return{bestBid:s,bestAsk:i,midPrice:r,bids:t,asks:e,timestamp:Date.now(),bidOrders:this.getBidOrders(),askOrders:this.getAskOrders(),dirtyChanges:n.dirtyChanges,structuralChange:n.structuralChange}}reset(){this.bidLevels.clear(),this.askLevels.clear(),this.orderIndex.clear(),this.dirtyChanges=[],this.hasStructuralChange=!1}getOrderCount(){return this.orderIndex.size}getLevelCount(){return this.bidLevels.size+this.askLevels.size}consumeDirtyState(){const t=this.dirtyChanges,e=this.hasStructuralChange;return this.dirtyChanges=[],this.hasStructuralChange=!1,{dirtyChanges:t,structuralChange:e}}recordDirtyChange(t,e,s,i){(s||i)&&(this.hasStructuralChange=!0),this.dirtyChanges.push({price:t,side:e,isRemoval:s,isAddition:i})}}class f{constructor(t){this.bids=new Map,this.asks=new Map,this.mboManager=new u,this.updateCount=0,this.lastRenderTime=0,this.dirtyChanges=[],this.hasStructuralChange=!1,this.rafId=0,this.container=t.container;const e=void 0===t.showVolumeBars||t.showVolumeBars,s=void 0===t.showOrderCount||t.showOrderCount,i=Math.round((s?5:3)*r+(e?r*n:0));this.config={container:t.container,width:t.width||i,height:t.height||600,rowHeight:t.rowHeight||24,visibleLevels:t.visibleLevels||50,tickSize:t.tickSize||.01,mode:t.mode||"PriceLevel",readOnly:t.readOnly||!1,showVolumeBars:e,showOrderCount:s,colors:t.colors||a,onTrade:t.onTrade||(()=>{}),onPriceHover:t.onPriceHover||(()=>{})},this.dataMode=this.config.mode,this.canvas=document.createElement("canvas"),this.canvas.style.display="block",this.container.appendChild(this.canvas),this.renderer=new c(this.canvas,this.config.width,this.config.height,this.config.rowHeight,this.config.colors,this.config.showVolumeBars,this.config.showOrderCount,this.config.tickSize),this.interactionHandler=new l(this.canvas,this.renderer,this.config.readOnly),this.setupInteractions(),this.startRenderLoop()}setupInteractions(){this.interactionHandler.onPriceClick=(t,e)=>{this.config.onTrade?.(t,e)},this.interactionHandler.onPriceHover=t=>{this.config.onPriceHover?.(t)},this.interactionHandler.onScroll=t=>{const e=this.renderer.getScrollOffset();this.renderer.setScrollOffset(e+t)}}processUpdate(e){if("PriceLevel"!==this.dataMode)return;const s=e.side===t.BID?this.bids:this.asks,i=s.has(e.price);let r=!1,n=!1;const o={price:e.price,quantity:e.quantity,numOrders:e.numOrders,side:e.side,isDirty:!0,hasOwnOrders:!1};e.quantity>0?(s.set(e.price,o),i||(r=!0,this.hasStructuralChange=!0)):i&&(s.delete(e.price),n=!0,this.hasStructuralChange=!0),(e.quantity>0||i)&&this.dirtyChanges.push({price:e.price,side:e.side,isRemoval:n,isAddition:r}),this.updateCount++}processBatch(t){if("PriceLevel"===this.dataMode)for(const e of t)this.processUpdate(e)}processUpdateBinary(t){console.warn("Binary updates not yet implemented, use processUpdate() instead")}processOrderUpdate(t,e){if("MBO"!==this.dataMode)return;const s={...t,price:this.roundToTick(t.price)};this.mboManager.processOrderUpdate(s,e),this.updateCount++}processOrderBatch(t){if("MBO"===this.dataMode)for(const e of t){const t={...e.update,price:this.roundToTick(e.update.price)};this.mboManager.processOrderUpdate(t,e.type),this.updateCount++}}getSnapshot(){if("MBO"===this.dataMode){const t=this.mboManager.getBidLevels(),e=this.mboManager.getAskLevels(),s=t.length>0?t[t.length-1].price:null,i=e.length>0?e[0].price:null,r=null!==s&&null!==i?(s+i)/2:null,n=this.mboManager.consumeDirtyState();return{bestBid:s,bestAsk:i,midPrice:r,bids:t,asks:e,timestamp:Date.now(),bidOrders:this.mboManager.getBidOrders(),askOrders:this.mboManager.getAskOrders(),dirtyChanges:n.dirtyChanges,structuralChange:n.structuralChange}}const t=Array.from(this.bids.values()).sort((t,e)=>t.price-e.price),e=Array.from(this.asks.values()).sort((t,e)=>t.price-e.price),s=t.length>0?t[t.length-1].price:null,i=e.length>0?e[0].price:null,r=null!==s&&null!==i?(s+i)/2:null,n=this.dirtyChanges,o=this.hasStructuralChange;return this.dirtyChanges=[],this.hasStructuralChange=!1,{bestBid:s,bestAsk:i,midPrice:r,bids:t,asks:e,timestamp:Date.now(),dirtyChanges:n,structuralChange:o}}startRenderLoop(){const t=e=>{if(e-this.lastRenderTime>=16.67){const t=this.getSnapshot();this.renderer.render(t),this.lastRenderTime=e}this.rafId=requestAnimationFrame(t)};this.rafId=requestAnimationFrame(t)}getBestBid(){if("MBO"===this.dataMode){const t=this.mboManager.getBidLevels();return t.length>0?t[t.length-1].price:null}const t=Array.from(this.bids.values()).sort((t,e)=>t.price-e.price);return t.length>0?t[t.length-1].price:null}getBestAsk(){if("MBO"===this.dataMode){const t=this.mboManager.getAskLevels();return t.length>0?t[0].price:null}const t=Array.from(this.asks.values()).sort((t,e)=>t.price-e.price);return t.length>0?t[0].price:null}getMidPrice(){const t=this.getBestBid(),e=this.getBestAsk();return null!==t&&null!==e?(t+e)/2:null}getSpread(){const t=this.getBestBid(),e=this.getBestAsk();return null!==t&&null!==e?e-t:null}getMetrics(){return"MBO"===this.dataMode?{...this.renderer.getMetrics(),updateCount:this.updateCount,bidLevels:this.mboManager.getBidLevels().length,askLevels:this.mboManager.getAskLevels().length}:{...this.renderer.getMetrics(),updateCount:this.updateCount,bidLevels:this.bids.size,askLevels:this.asks.size}}resize(t,e){t&&(this.config.width=t),e&&(this.config.height=e),this.renderer.resize(this.config.width,this.config.height)}setReadOnly(t){this.config.readOnly=t,this.interactionHandler.setReadOnly(t)}calculateWidth(){const t=this.config.showOrderCount?5:3,e=this.config.showVolumeBars?r*n:0;return Math.round(t*r+e)}setShowVolumeBars(t){this.config.showVolumeBars=t;const e=this.calculateWidth();this.config.width=e,this.renderer=new c(this.canvas,this.config.width,this.config.height,this.config.rowHeight,this.config.colors,this.config.showVolumeBars,this.config.showOrderCount,this.config.tickSize),this.interactionHandler.setRenderer(this.renderer);const s=this.getSnapshot();this.renderer.render(s)}setShowOrderCount(t){this.config.showOrderCount=t;const e=this.calculateWidth();this.config.width=e,this.renderer=new c(this.canvas,this.config.width,this.config.height,this.config.rowHeight,this.config.colors,this.config.showVolumeBars,this.config.showOrderCount,this.config.tickSize),this.interactionHandler.setRenderer(this.renderer);const s=this.getSnapshot();this.renderer.render(s)}setDataMode(t){if(this.dataMode===t)return;this.dataMode=t,this.config.mode=t,this.clear();const e=this.getSnapshot();this.renderer.render(e)}clear(){this.bids.clear(),this.asks.clear(),this.mboManager.reset(),this.updateCount=0,this.dirtyChanges=[],this.hasStructuralChange=!1,this.renderer.resetCenterPrice()}roundToTick(t){const e=this.config.tickSize||.01;return Math.round(t/e)*e}destroy(){cancelAnimationFrame(this.rafId),this.interactionHandler.destroy(),this.container.removeChild(this.canvas)}}return"undefined"!=typeof window&&(window.PriceLadder=f),i})());
|