@deeptick/client 0.1.1

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.
@@ -0,0 +1,468 @@
1
+ /**
2
+ * DeepTick TypeScript Client — Real-time Streaming
3
+ *
4
+ * Connects to the DeepTick WebSocket endpoint and delivers live market data.
5
+ * Works in both Node.js (via `ws` package) and browser environments.
6
+ *
7
+ * Install:
8
+ * npm install @deeptick/client # includes streaming
9
+ * # Node.js also needs: npm install ws
10
+ *
11
+ * Usage:
12
+ * import { DeepTickStream } from '@deeptick/client';
13
+ *
14
+ * const stream = new DeepTickStream('wss://deeptick.lacertalabs.xyz/v1/stream');
15
+ *
16
+ * stream.onTrade((exchange, symbol, trade) => {
17
+ * console.log(`[${exchange}] ${symbol}: ${trade.price} x ${trade.amount}`);
18
+ * });
19
+ *
20
+ * stream.onBookTicker((exchange, symbol, ticker) => {
21
+ * console.log(`[${exchange}] ${symbol}: ${ticker.bid_price}/${ticker.ask_price}`);
22
+ * });
23
+ *
24
+ * await stream.connect();
25
+ * await stream.subscribe(['hyperliquid.trades.BTC', '*.book_ticker.*']);
26
+ */
27
+
28
+ // ─── Types ──────────────────────────────────────────────────────────
29
+
30
+ /** Wire protocol frame from server */
31
+ export interface StreamFrame {
32
+ type: 'data' | 'heartbeat' | 'welcome' | 'subscribed' | 'unsubscribed' | 'pong' | 'error';
33
+ topic?: string;
34
+ data?: Record<string, any>;
35
+ ts?: number;
36
+ connection_id?: string;
37
+ channels?: string[];
38
+ message?: string;
39
+ client_ts?: number;
40
+ }
41
+
42
+ /** Trade data from stream */
43
+ export interface StreamTrade {
44
+ exchange: string;
45
+ symbol: string;
46
+ timestamp: number;
47
+ local_timestamp: number;
48
+ trade_id: string;
49
+ price: number;
50
+ amount: number;
51
+ side: 'buy' | 'sell';
52
+ data_type: 'trades';
53
+ }
54
+
55
+ /** Native top-of-book ticker data from stream */
56
+ export interface StreamBookTicker {
57
+ exchange: string;
58
+ symbol: string;
59
+ timestamp: number;
60
+ local_timestamp: number;
61
+ bid_price: number;
62
+ bid_amount: number;
63
+ ask_price: number;
64
+ ask_amount: number;
65
+ data_type: 'book_ticker';
66
+ }
67
+
68
+ /** Book delta data from stream */
69
+ export interface StreamBookDelta {
70
+ exchange: string;
71
+ symbol: string;
72
+ timestamp: number;
73
+ local_timestamp: number;
74
+ is_snapshot: boolean;
75
+ bids_packed: string; // base64-encoded binary
76
+ asks_packed: string;
77
+ data_type: 'book_l2_delta';
78
+ }
79
+
80
+ /** Derivative ticker data from stream */
81
+ export interface StreamDerivativeTicker {
82
+ exchange: string;
83
+ symbol: string;
84
+ timestamp: number;
85
+ local_timestamp: number;
86
+ funding_timestamp: number | null;
87
+ funding_rate: number | null;
88
+ predicted_funding_rate: number | null;
89
+ open_interest: number | null;
90
+ last_price: number | null;
91
+ mark_price: number | null;
92
+ index_price: number | null;
93
+ data_type: 'derivative_ticker';
94
+ }
95
+
96
+ /** Callback signature for data handlers */
97
+ export type DataCallback<T = Record<string, any>> = (
98
+ exchange: string,
99
+ symbol: string,
100
+ data: T
101
+ ) => void;
102
+
103
+ /** Streaming client options */
104
+ export interface DeepTickStreamOptions {
105
+ /** WebSocket URL (default: wss://deeptick.lacertalabs.xyz/v1/stream) */
106
+ url?: string;
107
+ /** Auto-reconnect on disconnect (default: true) */
108
+ reconnect?: boolean;
109
+ /** Initial reconnect delay in ms (default: 1000) */
110
+ reconnectDelay?: number;
111
+ /** Max reconnect delay in ms (default: 30000) */
112
+ reconnectMaxDelay?: number;
113
+ /** Max queued messages before dropping (default: 10000) */
114
+ maxQueueSize?: number;
115
+ }
116
+
117
+ // ─── Streaming Client ───────────────────────────────────────────────
118
+
119
+ export class DeepTickStream {
120
+ private url: string;
121
+ private reconnect: boolean;
122
+ private reconnectDelay: number;
123
+ private reconnectMaxDelay: number;
124
+ private currentDelay: number;
125
+ private maxQueueSize: number;
126
+
127
+ private ws: WebSocket | null = null;
128
+ private connected = false;
129
+ private stopped = false;
130
+ private channels: string[] = [];
131
+ private connectionId: string | null = null;
132
+
133
+ // Callbacks by data type
134
+ private tradeCallbacks: DataCallback<StreamTrade>[] = [];
135
+ private bookTickerCallbacks: DataCallback<StreamBookTicker>[] = [];
136
+ private bookDeltaCallbacks: DataCallback<StreamBookDelta>[] = [];
137
+ private derivativeTickerCallbacks: DataCallback<StreamDerivativeTicker>[] = [];
138
+ private liquidationCallbacks: DataCallback[] = [];
139
+ private globalCallbacks: DataCallback[] = [];
140
+
141
+ // Event callbacks
142
+ private connectCallbacks: (() => void)[] = [];
143
+ private disconnectCallbacks: ((reason: string) => void)[] = [];
144
+ private errorCallbacks: ((error: Error) => void)[] = [];
145
+
146
+ // Metrics
147
+ public messagesReceived = 0;
148
+ public reconnectCount = 0;
149
+ public lastMessageTime = 0;
150
+
151
+ // Pending resolve for subscribe/connect
152
+ private pendingSubscribe: ((value: void) => void) | null = null;
153
+ private pendingConnect: ((value: void) => void) | null = null;
154
+
155
+ constructor(urlOrOptions?: string | DeepTickStreamOptions) {
156
+ const opts: DeepTickStreamOptions =
157
+ typeof urlOrOptions === 'string' ? { url: urlOrOptions } : urlOrOptions ?? {};
158
+
159
+ this.url = opts.url ?? 'wss://deeptick.lacertalabs.xyz/v1/stream';
160
+ this.reconnect = opts.reconnect ?? true;
161
+ this.reconnectDelay = opts.reconnectDelay ?? 1000;
162
+ this.reconnectMaxDelay = opts.reconnectMaxDelay ?? 30_000;
163
+ this.currentDelay = this.reconnectDelay;
164
+ this.maxQueueSize = opts.maxQueueSize ?? 10_000;
165
+ }
166
+
167
+ // ─── Callback Registration ──────────────────────────────────────
168
+
169
+ /** Register callback for trade data */
170
+ onTrade(cb: DataCallback<StreamTrade>): this {
171
+ this.tradeCallbacks.push(cb);
172
+ return this;
173
+ }
174
+
175
+ /** Register callback for native top-of-book ticker data */
176
+ onBookTicker(cb: DataCallback<StreamBookTicker>): this {
177
+ this.bookTickerCallbacks.push(cb);
178
+ return this;
179
+ }
180
+
181
+ /** Deprecated alias for onBookTicker. */
182
+ onBBO(cb: DataCallback<StreamBookTicker>): this {
183
+ return this.onBookTicker(cb);
184
+ }
185
+
186
+ /** Register callback for book delta data */
187
+ onBookDelta(cb: DataCallback<StreamBookDelta>): this {
188
+ this.bookDeltaCallbacks.push(cb);
189
+ return this;
190
+ }
191
+
192
+ /** Register callback for derivative ticker data */
193
+ onDerivativeTicker(cb: DataCallback<StreamDerivativeTicker>): this {
194
+ this.derivativeTickerCallbacks.push(cb);
195
+ return this;
196
+ }
197
+
198
+ /** Deprecated alias for onDerivativeTicker. */
199
+ onFunding(cb: DataCallback<StreamDerivativeTicker>): this {
200
+ return this.onDerivativeTicker(cb);
201
+ }
202
+
203
+ /** Deprecated alias for onDerivativeTicker. */
204
+ onOpenInterest(cb: DataCallback<StreamDerivativeTicker>): this {
205
+ return this.onDerivativeTicker(cb);
206
+ }
207
+
208
+ /** Register callback for liquidation data */
209
+ onLiquidation(cb: DataCallback): this {
210
+ this.liquidationCallbacks.push(cb);
211
+ return this;
212
+ }
213
+
214
+ /** Register callback for ALL data types */
215
+ onData(cb: DataCallback): this {
216
+ this.globalCallbacks.push(cb);
217
+ return this;
218
+ }
219
+
220
+ /** Register callback for connection events */
221
+ onConnect(cb: () => void): this {
222
+ this.connectCallbacks.push(cb);
223
+ return this;
224
+ }
225
+
226
+ /** Register callback for disconnection events */
227
+ onDisconnect(cb: (reason: string) => void): this {
228
+ this.disconnectCallbacks.push(cb);
229
+ return this;
230
+ }
231
+
232
+ /** Register callback for errors */
233
+ onError(cb: (error: Error) => void): this {
234
+ this.errorCallbacks.push(cb);
235
+ return this;
236
+ }
237
+
238
+ // ─── Connection Management ──────────────────────────────────────
239
+
240
+ /** Connect to the DeepTick streaming endpoint */
241
+ async connect(): Promise<void> {
242
+ this.stopped = false;
243
+
244
+ return new Promise((resolve, reject) => {
245
+ try {
246
+ // Use browser WebSocket or Node.js ws
247
+ this.ws = new WebSocket(this.url);
248
+
249
+ this.ws.onopen = () => {
250
+ // Wait for welcome message before resolving
251
+ this.pendingConnect = resolve;
252
+ };
253
+
254
+ this.ws.onmessage = (event: MessageEvent) => {
255
+ this.handleMessage(event.data as string);
256
+ };
257
+
258
+ this.ws.onclose = (event: CloseEvent) => {
259
+ const reason = event.reason || `code ${event.code}`;
260
+ this.connected = false;
261
+ this.disconnectCallbacks.forEach(cb => cb(reason));
262
+
263
+ if (this.reconnect && !this.stopped) {
264
+ this.scheduleReconnect();
265
+ }
266
+ };
267
+
268
+ this.ws.onerror = (event: Event) => {
269
+ const error = new Error('WebSocket error');
270
+ this.errorCallbacks.forEach(cb => cb(error));
271
+ if (!this.connected) {
272
+ reject(error);
273
+ }
274
+ };
275
+ } catch (err) {
276
+ reject(err);
277
+ }
278
+ });
279
+ }
280
+
281
+ /** Disconnect from the streaming endpoint */
282
+ disconnect(): void {
283
+ this.stopped = true;
284
+ this.connected = false;
285
+ if (this.ws) {
286
+ this.ws.close();
287
+ this.ws = null;
288
+ }
289
+ }
290
+
291
+ /** Subscribe to data channels */
292
+ async subscribe(channels: string[]): Promise<void> {
293
+ this.channels = channels;
294
+ if (!this.connected || !this.ws) {
295
+ throw new Error('Not connected. Call connect() first.');
296
+ }
297
+
298
+ return new Promise<void>((resolve) => {
299
+ this.pendingSubscribe = resolve;
300
+ this.ws!.send(JSON.stringify({
301
+ action: 'subscribe',
302
+ channels,
303
+ }));
304
+ });
305
+ }
306
+
307
+ /** Unsubscribe from specific channels */
308
+ unsubscribe(channels: string[]): void {
309
+ this.channels = this.channels.filter(c => !channels.includes(c));
310
+ if (this.connected && this.ws) {
311
+ this.ws.send(JSON.stringify({
312
+ action: 'unsubscribe',
313
+ channels,
314
+ }));
315
+ }
316
+ }
317
+
318
+ /** Send ping and measure round-trip latency (ms) */
319
+ async ping(): Promise<number> {
320
+ if (!this.connected || !this.ws) {
321
+ throw new Error('Not connected');
322
+ }
323
+
324
+ const t0 = Date.now();
325
+ return new Promise<number>((resolve) => {
326
+ const handler = (event: MessageEvent) => {
327
+ const frame = JSON.parse(event.data as string) as StreamFrame;
328
+ if (frame.type === 'pong') {
329
+ this.ws?.removeEventListener('message', handler as any);
330
+ resolve(Date.now() - t0);
331
+ }
332
+ };
333
+ this.ws!.addEventListener('message', handler as any);
334
+ this.ws!.send(JSON.stringify({
335
+ action: 'ping',
336
+ ts: t0 * 1_000_000, // ns
337
+ }));
338
+ });
339
+ }
340
+
341
+ /** Get streaming client statistics */
342
+ getStats(): {
343
+ connected: boolean;
344
+ connectionId: string | null;
345
+ url: string;
346
+ channels: string[];
347
+ messagesReceived: number;
348
+ reconnectCount: number;
349
+ lastMessageAgeMs: number;
350
+ } {
351
+ return {
352
+ connected: this.connected,
353
+ connectionId: this.connectionId,
354
+ url: this.url,
355
+ channels: this.channels,
356
+ messagesReceived: this.messagesReceived,
357
+ reconnectCount: this.reconnectCount,
358
+ lastMessageAgeMs: this.lastMessageTime > 0
359
+ ? Date.now() - this.lastMessageTime
360
+ : -1,
361
+ };
362
+ }
363
+
364
+ // ─── Internal ───────────────────────────────────────────────────
365
+
366
+ private handleMessage(raw: string): void {
367
+ let frame: StreamFrame;
368
+ try {
369
+ frame = JSON.parse(raw);
370
+ } catch {
371
+ return;
372
+ }
373
+
374
+ this.messagesReceived++;
375
+ this.lastMessageTime = Date.now();
376
+
377
+ switch (frame.type) {
378
+ case 'welcome':
379
+ this.connectionId = frame.connection_id ?? null;
380
+ this.connected = true;
381
+ this.currentDelay = this.reconnectDelay;
382
+ this.connectCallbacks.forEach(cb => cb());
383
+ // Resolve pending connect promise
384
+ if (this.pendingConnect) {
385
+ this.pendingConnect();
386
+ this.pendingConnect = null;
387
+ }
388
+ // Resubscribe on reconnect
389
+ if (this.channels.length > 0) {
390
+ this.ws?.send(JSON.stringify({
391
+ action: 'subscribe',
392
+ channels: this.channels,
393
+ }));
394
+ }
395
+ break;
396
+
397
+ case 'subscribed':
398
+ if (this.pendingSubscribe) {
399
+ this.pendingSubscribe();
400
+ this.pendingSubscribe = null;
401
+ }
402
+ break;
403
+
404
+ case 'data':
405
+ this.dispatchData(frame.topic ?? '', frame.data ?? {});
406
+ break;
407
+
408
+ case 'heartbeat':
409
+ // Connection alive
410
+ break;
411
+
412
+ case 'error':
413
+ const error = new Error(frame.message ?? 'Unknown server error');
414
+ this.errorCallbacks.forEach(cb => cb(error));
415
+ break;
416
+ }
417
+ }
418
+
419
+ private dispatchData(topic: string, data: Record<string, any>): void {
420
+ // Parse topic: "exchange.data_type.symbol"
421
+ const parts = topic.split('.', 3);
422
+ if (parts.length < 3) return;
423
+
424
+ const [exchange, dataType, symbol] = parts;
425
+
426
+ // Route to type-specific callbacks
427
+ switch (dataType) {
428
+ case 'trades':
429
+ this.tradeCallbacks.forEach(cb => cb(exchange, symbol, data as StreamTrade));
430
+ break;
431
+ case 'book_ticker':
432
+ this.bookTickerCallbacks.forEach(cb => cb(exchange, symbol, data as StreamBookTicker));
433
+ break;
434
+ case 'book_l2_delta':
435
+ this.bookDeltaCallbacks.forEach(cb => cb(exchange, symbol, data as StreamBookDelta));
436
+ break;
437
+ case 'derivative_ticker':
438
+ this.derivativeTickerCallbacks.forEach(cb => cb(exchange, symbol, data as StreamDerivativeTicker));
439
+ break;
440
+ case 'liquidations':
441
+ this.liquidationCallbacks.forEach(cb => cb(exchange, symbol, data));
442
+ break;
443
+ }
444
+
445
+ // Global callbacks
446
+ this.globalCallbacks.forEach(cb => cb(exchange, symbol, data));
447
+ }
448
+
449
+ private scheduleReconnect(): void {
450
+ this.reconnectCount++;
451
+ console.log(
452
+ `[deeptick] Reconnecting in ${this.currentDelay}ms (attempt #${this.reconnectCount})`
453
+ );
454
+
455
+ setTimeout(async () => {
456
+ try {
457
+ await this.connect();
458
+ } catch {
459
+ // connect() failed, onclose will fire and trigger another retry
460
+ }
461
+ }, this.currentDelay);
462
+
463
+ this.currentDelay = Math.min(this.currentDelay * 2, this.reconnectMaxDelay);
464
+ }
465
+ }
466
+
467
+ // ─── Re-export types from types.ts ──────────────────────────────────
468
+ export type { TradeRecord, BBORecord, BookDeltaRecord, CandleRecord } from './types.js';
package/src/types.ts ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * DeepTick TypeScript Client — Derived Data Computations
3
+ *
4
+ * All derived data types are computed client-side from raw Phase 1 data.
5
+ * Uses Apache Arrow JS for zero-copy Parquet reads.
6
+ *
7
+ * Install: npm install @deeptick/client apache-arrow parquet-wasm
8
+ *
9
+ * Usage:
10
+ * import { DeepTick, derive } from '@deeptick/client';
11
+ *
12
+ * const dt = new DeepTick({ dataDir: './data/merged' });
13
+ * const trades = await dt.loadTrades('hyperliquid', 'BTC', '2026-05-01');
14
+ * const candles = derive.candles(trades, { intervalMs: 60_000 });
15
+ * const bbo = derive.bbo(await dt.loadBookDeltas('hyperliquid', 'BTC', '2026-05-01'));
16
+ */
17
+
18
+ // ─── Types ──────────────────────────────────────────────────────────
19
+
20
+ export interface TradeRecord {
21
+ timestamp: bigint;
22
+ local_timestamp: bigint;
23
+ price: number;
24
+ amount: number;
25
+ side: 'buy' | 'sell';
26
+ trade_id: string;
27
+ }
28
+
29
+ export interface BookDeltaRecord {
30
+ timestamp: bigint;
31
+ local_timestamp: bigint;
32
+ is_snapshot: boolean;
33
+ bids_packed: Uint8Array; // binary-packed float64 pairs
34
+ asks_packed: Uint8Array;
35
+ bid_count: number;
36
+ ask_count: number;
37
+ sequence: bigint | null;
38
+ }
39
+
40
+ export interface BBORecord {
41
+ timestamp: bigint;
42
+ local_timestamp: bigint;
43
+ bid_price: number;
44
+ bid_amount: number;
45
+ ask_price: number;
46
+ ask_amount: number;
47
+ }
48
+
49
+ export interface CandleRecord {
50
+ timestamp: bigint;
51
+ open: number;
52
+ high: number;
53
+ low: number;
54
+ close: number;
55
+ volume: number;
56
+ buy_volume: number;
57
+ sell_volume: number;
58
+ trades_count: number;
59
+ vwap: number;
60
+ }
61
+
62
+ export interface BookSnapshot {
63
+ timestamp: bigint;
64
+ local_timestamp: bigint;
65
+ bids: [number, number][]; // [price, amount][]
66
+ asks: [number, number][];
67
+ bid_depth: number;
68
+ ask_depth: number;
69
+ }
70
+
71
+ export interface VolumeProfileBucket {
72
+ price_level: number;
73
+ volume: number;
74
+ buy_volume: number;
75
+ sell_volume: number;
76
+ trade_count: number;
77
+ }
78
+
79
+ export interface ImbalanceRecord {
80
+ timestamp: bigint;
81
+ imbalance: number; // -1.0 to +1.0
82
+ bid_volume: number;
83
+ ask_volume: number;
84
+ }
85
+
86
+ export interface FundingCrossRecord {
87
+ timestamp: bigint;
88
+ rates: Record<string, number>; // exchange → rate
89
+ spreads: Record<string, number>; // "exA_vs_exB" → spread
90
+ annualized_spreads: Record<string, number>; // annualized %
91
+ }
92
+
93
+ // ─── Derived Data Functions ─────────────────────────────────────────
94
+ // All functions are pure: raw data in → derived data out. Nothing stored.
95
+
96
+ export namespace derive {
97
+ /**
98
+ * Derive BBO time series from L2 book deltas.
99
+ * Replays delta stream maintaining full book state.
100
+ */
101
+ export function bbo(deltas: BookDeltaRecord[]): BBORecord[] {
102
+ const bids = new Map<number, number>();
103
+ const asks = new Map<number, number>();
104
+ const result: BBORecord[] = [];
105
+
106
+ for (const d of deltas) {
107
+ const bidsRaw = unpackLevels(d.bids_packed);
108
+ const asksRaw = unpackLevels(d.asks_packed);
109
+
110
+ if (d.is_snapshot) {
111
+ bids.clear();
112
+ asks.clear();
113
+ for (const [p, s] of bidsRaw) {
114
+ if (s > 0) bids.set(p, s);
115
+ }
116
+ for (const [p, s] of asksRaw) {
117
+ if (s > 0) asks.set(p, s);
118
+ }
119
+ } else {
120
+ for (const [p, s] of bidsRaw) {
121
+ if (s > 0) {
122
+ bids.set(p, s);
123
+ } else {
124
+ bids.delete(p);
125
+ }
126
+ }
127
+ for (const [p, s] of asksRaw) {
128
+ if (s > 0) {
129
+ asks.set(p, s);
130
+ } else {
131
+ asks.delete(p);
132
+ }
133
+ }
134
+ }
135
+
136
+ if (bids.size > 0 && asks.size > 0) {
137
+ let bestBidPx = -Infinity;
138
+ let bestBidSz = 0;
139
+ for (const [p, s] of bids.entries()) {
140
+ if (p > bestBidPx) {
141
+ bestBidPx = p;
142
+ bestBidSz = s;
143
+ }
144
+ }
145
+
146
+ let bestAskPx = Infinity;
147
+ let bestAskSz = 0;
148
+ for (const [p, s] of asks.entries()) {
149
+ if (p < bestAskPx) {
150
+ bestAskPx = p;
151
+ bestAskSz = s;
152
+ }
153
+ }
154
+
155
+ if (bestBidPx !== -Infinity && bestAskPx !== Infinity) {
156
+ result.push({
157
+ timestamp: d.timestamp,
158
+ local_timestamp: d.local_timestamp,
159
+ bid_price: bestBidPx,
160
+ bid_amount: bestBidSz,
161
+ ask_price: bestAskPx,
162
+ ask_amount: bestAskSz,
163
+ });
164
+ }
165
+ }
166
+ }
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Derive periodic L2 snapshots from delta stream.
172
+ * @param depth - 20 for standard, 1000 for deep book
173
+ * @param intervalMs - snapshot interval (default: 60000 = 1 min)
174
+ */
175
+ export function bookSnapshots(
176
+ deltas: BookDeltaRecord[],
177
+ options?: { depth?: number; intervalMs?: number }
178
+ ): BookSnapshot[] {
179
+ throw new Error("Not implemented yet. Feel free to contribute!");
180
+ }
181
+
182
+ /**
183
+ * Derive OHLCV candles from trades at any interval.
184
+ * Includes VWAP and buy/sell volume split.
185
+ */
186
+ export function candles(
187
+ trades: TradeRecord[],
188
+ options?: { intervalMs?: number }
189
+ ): CandleRecord[] {
190
+ throw new Error("Not implemented yet. Feel free to contribute!");
191
+ }
192
+
193
+ /**
194
+ * Derive volume profile from trades.
195
+ * @param priceBucketSize - auto-calculated if not provided
196
+ */
197
+ export function volumeProfile(
198
+ trades: TradeRecord[],
199
+ options?: { priceBucketSize?: number; numBuckets?: number }
200
+ ): VolumeProfileBucket[] {
201
+ throw new Error("Not implemented yet. Feel free to contribute!");
202
+ }
203
+
204
+ /**
205
+ * Derive order book imbalance from L2 deltas.
206
+ * imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol) at top N levels
207
+ */
208
+ export function bookImbalance(
209
+ deltas: BookDeltaRecord[],
210
+ options?: { depth?: number; intervalMs?: number }
211
+ ): ImbalanceRecord[] {
212
+ throw new Error("Not implemented yet. Feel free to contribute!");
213
+ }
214
+
215
+ /**
216
+ * Derive cross-exchange funding rate spreads.
217
+ * Takes funding data from multiple exchanges for same underlying.
218
+ */
219
+ export function fundingCross(
220
+ fundingByExchange: Record<string, { timestamp: bigint; funding_rate: number }[]>,
221
+ options?: { intervalMs?: number }
222
+ ): FundingCrossRecord[] {
223
+ throw new Error("Not implemented yet. Feel free to contribute!");
224
+ }
225
+
226
+ /**
227
+ * Derive cross-exchange basis (price spread in bps).
228
+ */
229
+ export function crossExchangeBasis(
230
+ tradesByExchange: Record<string, TradeRecord[]>,
231
+ options?: { intervalMs?: number }
232
+ ): { timestamp: bigint; prices: Record<string, number>; basis_bps: Record<string, number> }[] {
233
+ throw new Error("Not implemented yet. Feel free to contribute!");
234
+ }
235
+ }
236
+
237
+ // ─── Utility: Unpack binary-packed book levels ──────────────────────
238
+
239
+ export function unpackLevels(packed: Uint8Array): [number, number][] {
240
+ const view = new DataView(packed.buffer, packed.byteOffset, packed.byteLength);
241
+ const pairs: [number, number][] = [];
242
+ for (let i = 0; i < packed.byteLength; i += 16) {
243
+ pairs.push([view.getFloat64(i, true), view.getFloat64(i + 8, true)]);
244
+ }
245
+ return pairs;
246
+ }