@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 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})());