@tradejs/infra 2.0.18 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/ai.js +3 -54
  2. package/dist/ai.mjs +1 -54
  3. package/dist/chunk-2CZREG43.mjs +112 -0
  4. package/dist/chunk-DFMKDB2R.mjs +1285 -0
  5. package/dist/chunk-I2J6YDBD.mjs +910 -0
  6. package/dist/chunk-NWXFWTWU.mjs +1114 -0
  7. package/dist/chunk-SZQB7ER5.mjs +492 -0
  8. package/dist/chunk-YVIHTUV5.mjs +286 -0
  9. package/dist/internal-2coHaaos.d.mts +26 -0
  10. package/dist/internal-2coHaaos.d.ts +26 -0
  11. package/dist/runtimeDeployments.d.mts +10 -0
  12. package/dist/runtimeDeployments.d.ts +10 -0
  13. package/dist/runtimeDeployments.js +447 -0
  14. package/dist/runtimeDeployments.mjs +81 -0
  15. package/dist/runtimeStrategyConfigs.d.mts +28 -0
  16. package/dist/runtimeStrategyConfigs.d.ts +28 -0
  17. package/dist/runtimeStrategyConfigs.js +425 -0
  18. package/dist/runtimeStrategyConfigs.mjs +89 -0
  19. package/dist/timescale/candles.d.mts +38 -0
  20. package/dist/timescale/candles.d.ts +38 -0
  21. package/dist/timescale/candles.js +408 -0
  22. package/dist/timescale/candles.mjs +21 -0
  23. package/dist/timescale/client.d.mts +4 -0
  24. package/dist/timescale/client.d.ts +4 -0
  25. package/dist/timescale/client.js +109 -0
  26. package/dist/timescale/client.mjs +12 -0
  27. package/dist/timescale/derivatives.d.mts +90 -0
  28. package/dist/timescale/derivatives.d.ts +90 -0
  29. package/dist/timescale/derivatives.js +1270 -0
  30. package/dist/timescale/derivatives.mjs +26 -0
  31. package/dist/timescale/hyperliquidWhales.d.mts +149 -0
  32. package/dist/timescale/hyperliquidWhales.d.ts +149 -0
  33. package/dist/timescale/hyperliquidWhales.js +1893 -0
  34. package/dist/timescale/hyperliquidWhales.mjs +30 -0
  35. package/dist/timescale/marketContext.d.mts +188 -0
  36. package/dist/timescale/marketContext.d.ts +188 -0
  37. package/dist/timescale/marketContext.js +2091 -0
  38. package/dist/timescale/marketContext.mjs +60 -0
  39. package/dist/timescale/spread.d.mts +11 -0
  40. package/dist/timescale/spread.d.ts +11 -0
  41. package/dist/timescale/spread.js +215 -0
  42. package/dist/timescale/spread.mjs +11 -0
  43. package/dist/timescale.d.mts +9 -476
  44. package/dist/timescale.d.ts +9 -476
  45. package/dist/timescale.js +2121 -2112
  46. package/dist/timescale.mjs +73 -4070
  47. package/dist/tradingAccounts.d.mts +2 -8
  48. package/dist/tradingAccounts.d.ts +2 -8
  49. package/dist/tradingAccounts.js +0 -71
  50. package/dist/tradingAccounts.mjs +0 -65
  51. package/dist/values-BrvcmnfM.d.mts +6 -0
  52. package/dist/values-BrvcmnfM.d.ts +6 -0
  53. package/package.json +43 -2
@@ -1,4074 +1,77 @@
1
- // src/timescale.ts
2
- import { Pool } from "pg";
3
1
  import {
4
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION
5
- } from "@tradejs/types";
6
- var getPool = () => {
7
- if (!global.__pgPool__) {
8
- const host = process.env.PG_HOST || "127.0.0.1";
9
- const port = Number(process.env.PG_PORT ?? 5432);
10
- const user = process.env.PG_USER || "app";
11
- const password = String(process.env.PG_PASSWORD ?? "app");
12
- const database = process.env.PG_DATABASE || process.env.PG_DB || "app";
13
- const max = Number(process.env.PG_POOL_MAX ?? 10);
14
- const connectionTimeoutMillis = Number(
15
- process.env.PG_CONNECTION_TIMEOUT_MS ?? 3e4
16
- );
17
- global.__pgPool__ = new Pool({
18
- host,
19
- port,
20
- user,
21
- password,
22
- database,
23
- max: Number.isFinite(max) && max > 0 ? Math.floor(max) : 10,
24
- idleTimeoutMillis: 3e4,
25
- connectionTimeoutMillis: Number.isFinite(connectionTimeoutMillis) && connectionTimeoutMillis > 0 ? Math.floor(connectionTimeoutMillis) : 3e4
26
- });
27
- }
28
- return global.__pgPool__;
29
- };
30
- var candlesSchemaReady = false;
31
- var derivativesSchemaReady = false;
32
- var spreadSchemaReady = false;
33
- var binanceMarketSchemaReady = false;
34
- var hyperliquidWhaleSchemaReady = false;
35
- var candlesSchemaReadyPromise = null;
36
- var derivativesSchemaReadyPromise = null;
37
- var spreadSchemaReadyPromise = null;
38
- var binanceMarketSchemaReadyPromise = null;
39
- var hyperliquidWhaleSchemaReadyPromise = null;
40
- var marketContextSchemaMode = "ensure";
41
- var verifiedMarketContextSchemas = /* @__PURE__ */ new Set();
42
- var configureTimescaleMarketContextSchemaMode = (mode) => {
43
- marketContextSchemaMode = mode;
44
- verifiedMarketContextSchemas.clear();
45
- };
46
- var closeTimescalePool = async () => {
47
- const pool = global.__pgPool__;
48
- if (!pool) {
49
- return;
50
- }
51
- global.__pgPool__ = void 0;
52
- candlesSchemaReady = false;
53
- derivativesSchemaReady = false;
54
- spreadSchemaReady = false;
55
- binanceMarketSchemaReady = false;
56
- hyperliquidWhaleSchemaReady = false;
57
- candlesSchemaReadyPromise = null;
58
- derivativesSchemaReadyPromise = null;
59
- spreadSchemaReadyPromise = null;
60
- binanceMarketSchemaReadyPromise = null;
61
- hyperliquidWhaleSchemaReadyPromise = null;
62
- verifiedMarketContextSchemas.clear();
63
- await pool.end();
64
- };
65
- var CANDLES_SCHEMA_LOCK_KEY = 61e4;
66
- var DERIVATIVES_SCHEMA_LOCK_KEY = 610001;
67
- var SPREAD_SCHEMA_LOCK_KEY = 610002;
68
- var BINANCE_MARKET_SCHEMA_LOCK_KEY = 610003;
69
- var HYPERLIQUID_WHALE_SCHEMA_LOCK_KEY = 610004;
70
- var PG_SAFE_MAX_BIND_PARAMS = 3e4;
71
- var resolveMarketContextQueryTimeoutMs = (override) => {
72
- if (Number.isFinite(override) && Number(override) > 0) {
73
- return Math.floor(Number(override));
74
- }
75
- const configured = Number(process.env.MARKET_CONTEXT_SQL_TIMEOUT_MS);
76
- return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 3e4;
77
- };
78
- var createMarketContextQueryError = (name, message) => {
79
- const error = new Error(message);
80
- error.name = name;
81
- return error;
82
- };
83
- var queryMarketContext = async (text, values, options = {}) => {
84
- const pool = getPool();
85
- const client = await pool.connect();
86
- const timeoutMs = resolveMarketContextQueryTimeoutMs(options.timeoutMs);
87
- let released = false;
88
- let rejectCancellation;
89
- const release = (error) => {
90
- if (released) return;
91
- released = true;
92
- client.release(error);
93
- };
94
- const cancellation = new Promise((_resolve, reject) => {
95
- rejectCancellation = reject;
96
- });
97
- const cancel = (error) => {
98
- release(error);
99
- rejectCancellation?.(error);
100
- };
101
- const onAbort = () => cancel(
102
- createMarketContextQueryError(
103
- "AbortError",
104
- "Timescale market-context query aborted"
105
- )
106
- );
107
- const timer = setTimeout(
108
- () => cancel(
109
- createMarketContextQueryError(
110
- "TimescaleQueryTimeoutError",
111
- `Timescale market-context query exceeded ${timeoutMs}ms`
112
- )
113
- ),
114
- timeoutMs
115
- );
116
- timer.unref?.();
117
- options.signal?.addEventListener("abort", onAbort, { once: true });
118
- try {
119
- if (options.signal?.aborted) {
120
- const error = createMarketContextQueryError(
121
- "AbortError",
122
- "Timescale market-context query aborted"
123
- );
124
- release(error);
125
- throw error;
126
- }
127
- const query = client.query(text, values);
128
- return await Promise.race([query, cancellation]);
129
- } finally {
130
- clearTimeout(timer);
131
- options.signal?.removeEventListener("abort", onAbort);
132
- release();
133
- }
134
- };
135
- var normalizeCandleProvider = (provider) => String(provider || "").trim().toLowerCase();
136
- var normalizeCandleSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
137
- var getSafeBulkInsertRows = (columnsCount) => Math.max(1, Math.floor(PG_SAFE_MAX_BIND_PARAMS / columnsCount));
138
- var withSchemaLock = async (lockKey, work) => {
139
- const pool = getPool();
140
- await pool.query("SELECT pg_advisory_lock($1)", [lockKey]);
141
- try {
142
- await work();
143
- } finally {
144
- await pool.query("SELECT pg_advisory_unlock($1)", [lockKey]);
145
- }
146
- };
147
- var ensureCandlesSchema = async () => {
148
- if (candlesSchemaReady) return;
149
- if (candlesSchemaReadyPromise) {
150
- await candlesSchemaReadyPromise;
151
- return;
152
- }
153
- candlesSchemaReadyPromise = withSchemaLock(
154
- CANDLES_SCHEMA_LOCK_KEY,
155
- async () => {
156
- const pool = getPool();
157
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
158
- await pool.query(`
159
- CREATE TABLE IF NOT EXISTS candles (
160
- provider text NOT NULL DEFAULT 'bybit',
161
- symbol text NOT NULL,
162
- interval integer NOT NULL,
163
- ts timestamptz NOT NULL,
164
- open double precision NOT NULL,
165
- high double precision NOT NULL,
166
- low double precision NOT NULL,
167
- close double precision NOT NULL,
168
- volume double precision,
169
- turnover double precision,
170
- taker_buy_base_volume double precision,
171
- taker_buy_quote_volume double precision,
172
- taker_sell_base_volume double precision,
173
- taker_sell_quote_volume double precision,
174
- PRIMARY KEY (provider, symbol, interval, ts)
175
- )
176
- `);
177
- await pool.query(`
178
- SELECT create_hypertable(
179
- 'candles',
180
- 'ts',
181
- if_not_exists => TRUE,
182
- chunk_time_interval => interval '7 days'
183
- )
184
- `);
185
- await pool.query(`
186
- CREATE INDEX IF NOT EXISTS candles_provider_symbol_interval_ts_idx
187
- ON candles (provider, symbol, interval, ts DESC)
188
- `);
189
- await pool.query(`
190
- ALTER TABLE candles
191
- ADD COLUMN IF NOT EXISTS taker_buy_base_volume double precision,
192
- ADD COLUMN IF NOT EXISTS taker_buy_quote_volume double precision,
193
- ADD COLUMN IF NOT EXISTS taker_sell_base_volume double precision,
194
- ADD COLUMN IF NOT EXISTS taker_sell_quote_volume double precision
195
- `);
196
- candlesSchemaReady = true;
197
- }
198
- ).finally(() => {
199
- candlesSchemaReadyPromise = null;
200
- });
201
- await candlesSchemaReadyPromise;
202
- };
203
- var toRows = (provider, symbol, interval, data) => {
204
- const normalizedProvider = normalizeCandleProvider(provider);
205
- if (!normalizedProvider) {
206
- throw new Error("Candle provider is required");
207
- }
208
- const normalizedSymbol = normalizeCandleSymbol(symbol);
209
- return data.map((i) => ({
210
- provider: normalizedProvider,
211
- symbol: normalizedSymbol,
212
- interval,
213
- ts: new Date(i.timestamp),
214
- // ms -> Date
215
- open: i.open,
216
- high: i.high,
217
- low: i.low,
218
- close: i.close,
219
- volume: i.volume ?? null,
220
- turnover: i.turnover ?? null,
221
- takerBuyBaseVolume: i.takerBuyBaseVolume ?? null,
222
- takerBuyQuoteVolume: i.takerBuyQuoteVolume ?? null,
223
- takerSellBaseVolume: i.takerSellBaseVolume ?? null,
224
- takerSellQuoteVolume: i.takerSellQuoteVolume ?? null
225
- }));
226
- };
227
- async function upsertCandles(rows) {
228
- if (!rows.length) return;
229
- await ensureCandlesSchema();
230
- const pool = getPool();
231
- const cols = [
232
- "provider",
233
- "symbol",
234
- "interval",
235
- "ts",
236
- "open",
237
- "high",
238
- "low",
239
- "close",
240
- "volume",
241
- "turnover",
242
- "taker_buy_base_volume",
243
- "taker_buy_quote_volume",
244
- "taker_sell_base_volume",
245
- "taker_sell_quote_volume"
246
- ];
247
- const maxRows = Math.floor(65535 / cols.length);
248
- if (rows.length > maxRows) {
249
- for (let i = 0; i < rows.length; i += maxRows) {
250
- await upsertCandles(rows.slice(i, i + maxRows));
251
- }
252
- return;
253
- }
254
- const valuesSql = rows.map(
255
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
256
- ).join(",");
257
- const flat = rows.flatMap((r) => [
258
- normalizeCandleProvider(r.provider),
259
- normalizeCandleSymbol(r.symbol),
260
- r.interval,
261
- r.ts,
262
- r.open,
263
- r.high,
264
- r.low,
265
- r.close,
266
- r.volume ?? null,
267
- r.turnover ?? null,
268
- r.takerBuyBaseVolume ?? null,
269
- r.takerBuyQuoteVolume ?? null,
270
- r.takerSellBaseVolume ?? null,
271
- r.takerSellQuoteVolume ?? null
272
- ]);
273
- const sql = `
274
- INSERT INTO candles (${cols.join(",")})
275
- VALUES ${valuesSql}
276
- ON CONFLICT (provider, symbol, interval, ts) DO UPDATE SET
277
- open = EXCLUDED.open,
278
- high = EXCLUDED.high,
279
- low = EXCLUDED.low,
280
- close = EXCLUDED.close,
281
- volume = COALESCE(EXCLUDED.volume, candles.volume),
282
- turnover = COALESCE(EXCLUDED.turnover, candles.turnover),
283
- taker_buy_base_volume = COALESCE(EXCLUDED.taker_buy_base_volume, candles.taker_buy_base_volume),
284
- taker_buy_quote_volume = COALESCE(EXCLUDED.taker_buy_quote_volume, candles.taker_buy_quote_volume),
285
- taker_sell_base_volume = COALESCE(EXCLUDED.taker_sell_base_volume, candles.taker_sell_base_volume),
286
- taker_sell_quote_volume = COALESCE(EXCLUDED.taker_sell_quote_volume, candles.taker_sell_quote_volume)
287
- `;
288
- const client = await pool.connect();
289
- try {
290
- await client.query("BEGIN");
291
- await client.query(sql, flat);
292
- await client.query("COMMIT");
293
- } catch (e) {
294
- await client.query("ROLLBACK");
295
- throw e;
296
- } finally {
297
- client.release();
298
- }
299
- }
300
- var ensureDerivativesSchema = async () => {
301
- if (derivativesSchemaReady) return;
302
- if (derivativesSchemaReadyPromise) {
303
- await derivativesSchemaReadyPromise;
304
- return;
305
- }
306
- const pool = getPool();
307
- derivativesSchemaReadyPromise = withSchemaLock(
308
- DERIVATIVES_SCHEMA_LOCK_KEY,
309
- async () => {
310
- if (derivativesSchemaReady) return;
311
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
312
- await pool.query(`
313
- CREATE TABLE IF NOT EXISTS derivatives_market (
314
- symbol text NOT NULL,
315
- interval text NOT NULL,
316
- ts timestamptz NOT NULL,
317
- open_interest double precision,
318
- funding_rate double precision,
319
- liq_long double precision,
320
- liq_short double precision,
321
- liq_total double precision,
322
- source text,
323
- ingested_at timestamptz NOT NULL DEFAULT now(),
324
- PRIMARY KEY (symbol, interval, ts)
325
- )
326
- `);
327
- await pool.query(`
328
- SELECT create_hypertable(
329
- 'derivatives_market',
330
- 'ts',
331
- if_not_exists => TRUE,
332
- chunk_time_interval => interval '14 days'
333
- )
334
- `);
335
- await pool.query(`
336
- CREATE INDEX IF NOT EXISTS derivatives_market_symbol_tf_ts_idx
337
- ON derivatives_market (symbol, interval, ts DESC)
338
- `);
339
- await pool.query(`
340
- CREATE TABLE IF NOT EXISTS derivatives_backfill_coverage (
341
- source text NOT NULL,
342
- symbol text NOT NULL,
343
- interval text NOT NULL,
344
- from_ts timestamptz NOT NULL,
345
- to_ts timestamptz NOT NULL,
346
- rows_count integer NOT NULL DEFAULT 0,
347
- checked_at timestamptz NOT NULL DEFAULT now(),
348
- PRIMARY KEY (source, symbol, interval, from_ts, to_ts)
349
- )
350
- `);
351
- await pool.query(`
352
- CREATE INDEX IF NOT EXISTS derivatives_backfill_coverage_lookup_idx
353
- ON derivatives_backfill_coverage (source, symbol, interval, from_ts, to_ts)
354
- `);
355
- await pool.query(`
356
- CREATE TABLE IF NOT EXISTS derivatives_metric_coverage (
357
- source text NOT NULL,
358
- metric text NOT NULL,
359
- symbol text NOT NULL,
360
- interval text NOT NULL,
361
- from_ts timestamptz NOT NULL,
362
- to_ts timestamptz NOT NULL,
363
- event_rows_count integer NOT NULL DEFAULT 0,
364
- zero_rows_count integer NOT NULL DEFAULT 0,
365
- checked_at timestamptz NOT NULL DEFAULT now(),
366
- PRIMARY KEY (source, metric, symbol, interval, from_ts, to_ts)
367
- )
368
- `);
369
- await pool.query(`
370
- CREATE INDEX IF NOT EXISTS derivatives_metric_coverage_lookup_idx
371
- ON derivatives_metric_coverage (
372
- source,
373
- metric,
374
- symbol,
375
- interval,
376
- from_ts,
377
- to_ts
378
- )
379
- `);
380
- derivativesSchemaReady = true;
381
- }
382
- ).finally(() => {
383
- derivativesSchemaReadyPromise = null;
384
- });
385
- await derivativesSchemaReadyPromise;
386
- };
387
- var ensureSpreadSchema = async () => {
388
- if (spreadSchemaReady) return;
389
- if (spreadSchemaReadyPromise) {
390
- await spreadSchemaReadyPromise;
391
- return;
392
- }
393
- const pool = getPool();
394
- spreadSchemaReadyPromise = withSchemaLock(
395
- SPREAD_SCHEMA_LOCK_KEY,
396
- async () => {
397
- if (spreadSchemaReady) return;
398
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
399
- await pool.query(`
400
- CREATE TABLE IF NOT EXISTS market_spread (
401
- symbol text NOT NULL,
402
- interval text NOT NULL,
403
- ts timestamptz NOT NULL,
404
- binance_price double precision,
405
- coinbase_price double precision,
406
- spread double precision,
407
- source text,
408
- ingested_at timestamptz NOT NULL DEFAULT now(),
409
- PRIMARY KEY (symbol, interval, ts)
410
- )
411
- `);
412
- await pool.query(`
413
- SELECT create_hypertable(
414
- 'market_spread',
415
- 'ts',
416
- if_not_exists => TRUE,
417
- chunk_time_interval => interval '14 days'
418
- )
419
- `);
420
- await pool.query(`
421
- CREATE INDEX IF NOT EXISTS market_spread_symbol_tf_ts_idx
422
- ON market_spread (symbol, interval, ts DESC)
423
- `);
424
- spreadSchemaReady = true;
425
- }
426
- ).finally(() => {
427
- spreadSchemaReadyPromise = null;
428
- });
429
- await spreadSchemaReadyPromise;
430
- };
431
- var ensureBinanceMarketSchema = async () => {
432
- if (binanceMarketSchemaReady) return;
433
- if (binanceMarketSchemaReadyPromise) {
434
- await binanceMarketSchemaReadyPromise;
435
- return;
436
- }
437
- const pool = getPool();
438
- binanceMarketSchemaReadyPromise = withSchemaLock(
439
- BINANCE_MARKET_SCHEMA_LOCK_KEY,
440
- async () => {
441
- if (binanceMarketSchemaReady) return;
442
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
443
- await pool.query(`
444
- CREATE TABLE IF NOT EXISTS market_trade_flow (
445
- symbol text NOT NULL,
446
- interval text NOT NULL,
447
- ts timestamptz NOT NULL,
448
- trades integer NOT NULL,
449
- buy_base_volume double precision,
450
- sell_base_volume double precision,
451
- buy_quote_volume double precision,
452
- sell_quote_volume double precision,
453
- net_base_delta double precision,
454
- net_quote_delta double precision,
455
- buy_pressure_pct double precision,
456
- source text,
457
- ingested_at timestamptz NOT NULL DEFAULT now(),
458
- PRIMARY KEY (symbol, interval, ts)
459
- )
460
- `);
461
- await pool.query(`
462
- SELECT create_hypertable(
463
- 'market_trade_flow',
464
- 'ts',
465
- if_not_exists => TRUE,
466
- chunk_time_interval => interval '7 days'
467
- )
468
- `);
469
- await pool.query(`
470
- CREATE INDEX IF NOT EXISTS market_trade_flow_symbol_tf_ts_idx
471
- ON market_trade_flow (symbol, interval, ts DESC)
472
- `);
473
- await pool.query(`
474
- CREATE TABLE IF NOT EXISTS market_breadth (
475
- universe text NOT NULL,
476
- interval text NOT NULL,
477
- ts timestamptz NOT NULL,
478
- symbols_count integer NOT NULL,
479
- advancers integer NOT NULL,
480
- decliners integer NOT NULL,
481
- unchanged integer NOT NULL,
482
- advance_decline_ratio double precision,
483
- pct_above_ma20 double precision,
484
- pct_above_ma50 double precision,
485
- equal_weighted_return double precision,
486
- volume_weighted_return double precision,
487
- dispersion double precision,
488
- btc_return_1h double precision,
489
- btc_return_4h double precision,
490
- btc_return_24h double precision,
491
- alt_basket_return_1h double precision,
492
- alt_basket_return_4h double precision,
493
- alt_basket_return_24h double precision,
494
- btc_vs_alt_return_1h double precision,
495
- btc_vs_alt_return_4h double precision,
496
- btc_vs_alt_return_24h double precision,
497
- btc_turnover_share_1h double precision,
498
- btc_turnover_share_24h double precision,
499
- btc_turnover_share_change_24h double precision,
500
- alt_vol_to_btc_vol_24h double precision,
501
- alt_dispersion_24h double precision,
502
- btc_alt_regime text,
503
- source text,
504
- ingested_at timestamptz NOT NULL DEFAULT now(),
505
- PRIMARY KEY (universe, interval, ts)
506
- )
507
- `);
508
- await pool.query(`
509
- ALTER TABLE market_breadth
510
- ADD COLUMN IF NOT EXISTS btc_return_1h double precision,
511
- ADD COLUMN IF NOT EXISTS btc_return_4h double precision,
512
- ADD COLUMN IF NOT EXISTS btc_return_24h double precision,
513
- ADD COLUMN IF NOT EXISTS alt_basket_return_1h double precision,
514
- ADD COLUMN IF NOT EXISTS alt_basket_return_4h double precision,
515
- ADD COLUMN IF NOT EXISTS alt_basket_return_24h double precision,
516
- ADD COLUMN IF NOT EXISTS btc_vs_alt_return_1h double precision,
517
- ADD COLUMN IF NOT EXISTS btc_vs_alt_return_4h double precision,
518
- ADD COLUMN IF NOT EXISTS btc_vs_alt_return_24h double precision,
519
- ADD COLUMN IF NOT EXISTS btc_turnover_share_1h double precision,
520
- ADD COLUMN IF NOT EXISTS btc_turnover_share_24h double precision,
521
- ADD COLUMN IF NOT EXISTS btc_turnover_share_change_24h double precision,
522
- ADD COLUMN IF NOT EXISTS alt_vol_to_btc_vol_24h double precision,
523
- ADD COLUMN IF NOT EXISTS alt_dispersion_24h double precision,
524
- ADD COLUMN IF NOT EXISTS btc_alt_regime text
525
- `);
526
- await pool.query(`
527
- SELECT create_hypertable(
528
- 'market_breadth',
529
- 'ts',
530
- if_not_exists => TRUE,
531
- chunk_time_interval => interval '14 days'
532
- )
533
- `);
534
- await pool.query(`
535
- CREATE INDEX IF NOT EXISTS market_breadth_universe_tf_ts_idx
536
- ON market_breadth (universe, interval, ts DESC)
537
- `);
538
- await pool.query(`
539
- CREATE TABLE IF NOT EXISTS market_global_context (
540
- source text NOT NULL,
541
- ts timestamptz NOT NULL,
542
- updated_at_ts timestamptz,
543
- active_cryptocurrencies integer,
544
- active_exchanges integer,
545
- active_market_pairs integer,
546
- markets integer,
547
- total_market_cap_usd double precision,
548
- total_volume_usd double precision,
549
- total_volume_reported_usd double precision,
550
- btc_dominance_pct double precision,
551
- eth_dominance_pct double precision,
552
- alt_market_cap_usd double precision,
553
- alt_volume_usd double precision,
554
- alt_volume_reported_usd double precision,
555
- btc_to_alt_market_cap_ratio double precision,
556
- market_cap_change_pct_24h_usd double precision,
557
- ingested_at timestamptz NOT NULL DEFAULT now(),
558
- PRIMARY KEY (source, ts)
559
- )
560
- `);
561
- await pool.query(`
562
- SELECT create_hypertable(
563
- 'market_global_context',
564
- 'ts',
565
- if_not_exists => TRUE,
566
- chunk_time_interval => interval '30 days'
567
- )
568
- `);
569
- await pool.query(`
570
- CREATE INDEX IF NOT EXISTS market_global_context_source_ts_idx
571
- ON market_global_context (source, ts DESC)
572
- `);
573
- await pool.query(`
574
- ALTER TABLE market_global_context
575
- ADD COLUMN IF NOT EXISTS active_exchanges integer,
576
- ADD COLUMN IF NOT EXISTS active_market_pairs integer,
577
- ADD COLUMN IF NOT EXISTS total_volume_reported_usd double precision,
578
- ADD COLUMN IF NOT EXISTS alt_volume_usd double precision,
579
- ADD COLUMN IF NOT EXISTS alt_volume_reported_usd double precision
580
- `);
581
- await pool.query(`
582
- CREATE TABLE IF NOT EXISTS market_reference_asset_context (
583
- source text NOT NULL,
584
- symbol text NOT NULL,
585
- cmc_id integer NOT NULL,
586
- interval text NOT NULL,
587
- ts timestamptz NOT NULL,
588
- open_usd double precision,
589
- high_usd double precision,
590
- low_usd double precision,
591
- close_usd double precision,
592
- volume_usd double precision,
593
- market_cap_usd double precision,
594
- ingested_at timestamptz NOT NULL DEFAULT now(),
595
- PRIMARY KEY (source, symbol, interval, ts)
596
- )
597
- `);
598
- await pool.query(`
599
- SELECT create_hypertable(
600
- 'market_reference_asset_context',
601
- 'ts',
602
- if_not_exists => TRUE,
603
- chunk_time_interval => interval '30 days'
604
- )
605
- `);
606
- await pool.query(`
607
- CREATE INDEX IF NOT EXISTS market_reference_asset_context_lookup_idx
608
- ON market_reference_asset_context (source, symbol, interval, ts DESC)
609
- `);
610
- await pool.query(`
611
- CREATE TABLE IF NOT EXISTS market_cmc_exchange_liquidity_context (
612
- source text NOT NULL,
613
- interval text NOT NULL,
614
- ts timestamptz NOT NULL,
615
- exchanges_count integer NOT NULL,
616
- total_volume_usd double precision,
617
- binance_volume_usd double precision,
618
- binance_volume_share double precision,
619
- top_exchange_volume_share double precision,
620
- liquidity_regime text,
621
- ingested_at timestamptz NOT NULL DEFAULT now(),
622
- PRIMARY KEY (source, interval, ts)
623
- )
624
- `);
625
- await pool.query(`
626
- SELECT create_hypertable(
627
- 'market_cmc_exchange_liquidity_context',
628
- 'ts',
629
- if_not_exists => TRUE,
630
- chunk_time_interval => interval '30 days'
631
- )
632
- `);
633
- await pool.query(`
634
- CREATE INDEX IF NOT EXISTS market_cmc_exchange_liquidity_context_lookup_idx
635
- ON market_cmc_exchange_liquidity_context (source, interval, ts DESC)
636
- `);
637
- await pool.query(`
638
- CREATE TABLE IF NOT EXISTS market_cmc_fear_greed_context (
639
- source text NOT NULL,
640
- interval text NOT NULL,
641
- ts timestamptz NOT NULL,
642
- value integer NOT NULL,
643
- classification text NOT NULL,
644
- sentiment_regime text NOT NULL,
645
- ingested_at timestamptz NOT NULL DEFAULT now(),
646
- PRIMARY KEY (source, interval, ts)
647
- )
648
- `);
649
- await pool.query(`
650
- SELECT create_hypertable(
651
- 'market_cmc_fear_greed_context',
652
- 'ts',
653
- if_not_exists => TRUE,
654
- chunk_time_interval => interval '30 days'
655
- )
656
- `);
657
- await pool.query(`
658
- CREATE INDEX IF NOT EXISTS market_cmc_fear_greed_context_lookup_idx
659
- ON market_cmc_fear_greed_context (source, interval, ts DESC)
660
- `);
661
- await pool.query(`
662
- CREATE TABLE IF NOT EXISTS market_cmc_index_context (
663
- source text NOT NULL,
664
- index_slug text NOT NULL,
665
- interval text NOT NULL,
666
- ts timestamptz NOT NULL,
667
- value double precision NOT NULL,
668
- constituents_count integer,
669
- top_constituent_symbol text,
670
- top_constituent_weight_pct double precision,
671
- constituents jsonb,
672
- ingested_at timestamptz NOT NULL DEFAULT now(),
673
- PRIMARY KEY (source, index_slug, interval, ts)
674
- )
675
- `);
676
- await pool.query(`
677
- SELECT create_hypertable(
678
- 'market_cmc_index_context',
679
- 'ts',
680
- if_not_exists => TRUE,
681
- chunk_time_interval => interval '30 days'
682
- )
683
- `);
684
- await pool.query(`
685
- CREATE INDEX IF NOT EXISTS market_cmc_index_context_lookup_idx
686
- ON market_cmc_index_context (source, index_slug, interval, ts DESC)
687
- `);
688
- await pool.query(`
689
- CREATE TABLE IF NOT EXISTS market_context_backfill_coverage (
690
- source text NOT NULL,
691
- scope text NOT NULL,
692
- interval text NOT NULL,
693
- from_ts timestamptz NOT NULL,
694
- to_ts timestamptz NOT NULL,
695
- rows_count integer NOT NULL DEFAULT 0,
696
- checked_at timestamptz NOT NULL DEFAULT now(),
697
- PRIMARY KEY (source, scope, interval, from_ts, to_ts)
698
- )
699
- `);
700
- await pool.query(`
701
- CREATE INDEX IF NOT EXISTS market_context_backfill_coverage_lookup_idx
702
- ON market_context_backfill_coverage (source, scope, interval, from_ts, to_ts)
703
- `);
704
- binanceMarketSchemaReady = true;
705
- }
706
- ).finally(() => {
707
- binanceMarketSchemaReadyPromise = null;
708
- });
709
- await binanceMarketSchemaReadyPromise;
710
- };
711
- var ensureHyperliquidWhaleSchema = async () => {
712
- if (hyperliquidWhaleSchemaReady) return;
713
- if (hyperliquidWhaleSchemaReadyPromise) {
714
- await hyperliquidWhaleSchemaReadyPromise;
715
- return;
716
- }
717
- hyperliquidWhaleSchemaReadyPromise = withSchemaLock(
718
- HYPERLIQUID_WHALE_SCHEMA_LOCK_KEY,
719
- async () => {
720
- if (hyperliquidWhaleSchemaReady) return;
721
- const pool = getPool();
722
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
723
- await pool.query(`
724
- CREATE TABLE IF NOT EXISTS hyperliquid_whale_trade_events (
725
- symbol text NOT NULL,
726
- ts timestamptz NOT NULL,
727
- tid text NOT NULL,
728
- price double precision NOT NULL,
729
- size double precision NOT NULL,
730
- notional_usd double precision NOT NULL,
731
- buyer_address text,
732
- seller_address text,
733
- buyer_tracked boolean NOT NULL,
734
- seller_tracked boolean NOT NULL,
735
- buyer_start_position double precision,
736
- buyer_end_position double precision,
737
- buyer_position_action text,
738
- buyer_closed_pnl double precision,
739
- buyer_liquidation boolean,
740
- seller_start_position double precision,
741
- seller_end_position double precision,
742
- seller_position_action text,
743
- seller_closed_pnl double precision,
744
- seller_liquidation boolean,
745
- universe_fingerprint text NOT NULL,
746
- whale_registry_fingerprint text NOT NULL,
747
- source text,
748
- ingested_at timestamptz NOT NULL DEFAULT now(),
749
- PRIMARY KEY (
750
- universe_fingerprint,
751
- whale_registry_fingerprint,
752
- symbol,
753
- ts,
754
- tid
755
- )
756
- )
757
- `);
758
- await pool.query(`
759
- ALTER TABLE hyperliquid_whale_trade_events
760
- ADD COLUMN IF NOT EXISTS buyer_start_position double precision,
761
- ADD COLUMN IF NOT EXISTS buyer_end_position double precision,
762
- ADD COLUMN IF NOT EXISTS buyer_position_action text,
763
- ADD COLUMN IF NOT EXISTS buyer_closed_pnl double precision,
764
- ADD COLUMN IF NOT EXISTS buyer_liquidation boolean,
765
- ADD COLUMN IF NOT EXISTS seller_start_position double precision,
766
- ADD COLUMN IF NOT EXISTS seller_end_position double precision,
767
- ADD COLUMN IF NOT EXISTS seller_position_action text,
768
- ADD COLUMN IF NOT EXISTS seller_closed_pnl double precision,
769
- ADD COLUMN IF NOT EXISTS seller_liquidation boolean
770
- `);
771
- await pool.query(`
772
- SELECT create_hypertable(
773
- 'hyperliquid_whale_trade_events',
774
- 'ts',
775
- if_not_exists => TRUE,
776
- chunk_time_interval => interval '1 day'
777
- )
778
- `);
779
- await pool.query(`
780
- CREATE INDEX IF NOT EXISTS hyperliquid_whale_events_lookup_idx
781
- ON hyperliquid_whale_trade_events (
782
- universe_fingerprint,
783
- whale_registry_fingerprint,
784
- symbol,
785
- ts DESC
786
- )
787
- `);
788
- await pool.query(`
789
- CREATE TABLE IF NOT EXISTS hyperliquid_whale_flow (
790
- symbol text NOT NULL,
791
- interval text NOT NULL,
792
- ts timestamptz NOT NULL,
793
- trades integer NOT NULL,
794
- whale_sides integer NOT NULL,
795
- unique_whales integer NOT NULL,
796
- whale_addresses text[] NOT NULL DEFAULT '{}',
797
- buy_notional_usd double precision NOT NULL,
798
- sell_notional_usd double precision NOT NULL,
799
- net_notional_usd double precision NOT NULL,
800
- buy_share_pct double precision,
801
- position_aware_whale_sides integer NOT NULL DEFAULT 0,
802
- long_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
803
- short_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
804
- long_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
805
- short_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
806
- long_entry_notional_usd double precision NOT NULL DEFAULT 0,
807
- short_entry_notional_usd double precision NOT NULL DEFAULT 0,
808
- long_exit_notional_usd double precision NOT NULL DEFAULT 0,
809
- short_exit_notional_usd double precision NOT NULL DEFAULT 0,
810
- entry_net_notional_usd double precision NOT NULL DEFAULT 0,
811
- entry_long_share_pct double precision,
812
- universe_fingerprint text NOT NULL,
813
- whale_registry_fingerprint text NOT NULL,
814
- source text,
815
- ingested_at timestamptz NOT NULL DEFAULT now(),
816
- PRIMARY KEY (
817
- universe_fingerprint,
818
- whale_registry_fingerprint,
819
- symbol,
820
- interval,
821
- ts
822
- )
823
- )
824
- `);
825
- await pool.query(`
826
- ALTER TABLE hyperliquid_whale_flow
827
- ADD COLUMN IF NOT EXISTS position_aware_whale_sides integer NOT NULL DEFAULT 0,
828
- ADD COLUMN IF NOT EXISTS long_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
829
- ADD COLUMN IF NOT EXISTS short_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
830
- ADD COLUMN IF NOT EXISTS long_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
831
- ADD COLUMN IF NOT EXISTS short_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
832
- ADD COLUMN IF NOT EXISTS long_entry_notional_usd double precision NOT NULL DEFAULT 0,
833
- ADD COLUMN IF NOT EXISTS short_entry_notional_usd double precision NOT NULL DEFAULT 0,
834
- ADD COLUMN IF NOT EXISTS long_exit_notional_usd double precision NOT NULL DEFAULT 0,
835
- ADD COLUMN IF NOT EXISTS short_exit_notional_usd double precision NOT NULL DEFAULT 0,
836
- ADD COLUMN IF NOT EXISTS entry_net_notional_usd double precision NOT NULL DEFAULT 0,
837
- ADD COLUMN IF NOT EXISTS entry_long_share_pct double precision
838
- `);
839
- await pool.query(`
840
- SELECT create_hypertable(
841
- 'hyperliquid_whale_flow',
842
- 'ts',
843
- if_not_exists => TRUE,
844
- chunk_time_interval => interval '7 days'
845
- )
846
- `);
847
- await pool.query(`
848
- CREATE INDEX IF NOT EXISTS hyperliquid_whale_flow_lookup_idx
849
- ON hyperliquid_whale_flow (
850
- universe_fingerprint,
851
- whale_registry_fingerprint,
852
- symbol,
853
- interval,
854
- ts DESC
855
- )
856
- `);
857
- await pool.query(`
858
- CREATE TABLE IF NOT EXISTS hyperliquid_whale_wallet_coverage (
859
- universe_fingerprint text NOT NULL,
860
- whale_registry_fingerprint text NOT NULL,
861
- address text NOT NULL,
862
- requested_from_ts timestamptz NOT NULL,
863
- requested_to_ts timestamptz NOT NULL,
864
- covered_from_ts timestamptz,
865
- covered_to_ts timestamptz,
866
- status text NOT NULL CHECK (status IN ('complete', 'truncated', 'failed')),
867
- fills_count integer NOT NULL DEFAULT 0,
868
- error text,
869
- data_model_version integer NOT NULL DEFAULT 2,
870
- checked_at timestamptz NOT NULL DEFAULT now(),
871
- PRIMARY KEY (
872
- universe_fingerprint,
873
- whale_registry_fingerprint,
874
- address,
875
- requested_from_ts,
876
- requested_to_ts
877
- )
878
- )
879
- `);
880
- await pool.query(`
881
- ALTER TABLE hyperliquid_whale_wallet_coverage
882
- ADD COLUMN IF NOT EXISTS data_model_version integer NOT NULL DEFAULT 2
883
- `);
884
- await pool.query(`
885
- CREATE INDEX IF NOT EXISTS hyperliquid_whale_wallet_coverage_lookup_idx
886
- ON hyperliquid_whale_wallet_coverage (
887
- universe_fingerprint,
888
- whale_registry_fingerprint,
889
- address,
890
- requested_from_ts,
891
- requested_to_ts
892
- )
893
- `);
894
- await pool.query(`
895
- CREATE TABLE IF NOT EXISTS hyperliquid_whale_coverage_1m (
896
- ts timestamptz NOT NULL,
897
- covered_whales integer NOT NULL,
898
- expected_whales integer NOT NULL,
899
- coverage_pct double precision NOT NULL,
900
- universe_fingerprint text NOT NULL,
901
- whale_registry_fingerprint text NOT NULL,
902
- source text,
903
- data_model_version integer NOT NULL DEFAULT 2,
904
- ingested_at timestamptz NOT NULL DEFAULT now(),
905
- PRIMARY KEY (
906
- universe_fingerprint,
907
- whale_registry_fingerprint,
908
- ts
909
- )
910
- )
911
- `);
912
- await pool.query(`
913
- ALTER TABLE hyperliquid_whale_coverage_1m
914
- ADD COLUMN IF NOT EXISTS data_model_version integer NOT NULL DEFAULT 2
915
- `);
916
- await pool.query(`
917
- SELECT create_hypertable(
918
- 'hyperliquid_whale_coverage_1m',
919
- 'ts',
920
- if_not_exists => TRUE,
921
- chunk_time_interval => interval '7 days'
922
- )
923
- `);
924
- await pool.query(`
925
- CREATE INDEX IF NOT EXISTS hyperliquid_whale_coverage_1m_lookup_idx
926
- ON hyperliquid_whale_coverage_1m (
927
- universe_fingerprint,
928
- whale_registry_fingerprint,
929
- ts DESC
930
- )
931
- `);
932
- hyperliquidWhaleSchemaReady = true;
933
- }
934
- ).finally(() => {
935
- hyperliquidWhaleSchemaReadyPromise = null;
936
- });
937
- await hyperliquidWhaleSchemaReadyPromise;
938
- };
939
- var ensureCoinMarketCapContextSchema = async () => ensureBinanceMarketSchema();
940
- var ensureMarketContextSchema = async (source) => {
941
- switch (source) {
942
- case "binance":
943
- return ensureBinanceMarketSchema();
944
- case "coinmarketcap":
945
- return ensureCoinMarketCapContextSchema();
946
- case "derivatives":
947
- return ensureDerivativesSchema();
948
- case "hyperliquidWhales":
949
- return ensureHyperliquidWhaleSchema();
950
- }
951
- };
952
- var MARKET_CONTEXT_SCHEMA_TABLES = {
953
- binance: ["market_trade_flow", "market_breadth"],
954
- coinmarketcap: [
955
- "market_global_context",
956
- "market_reference_asset_context",
957
- "market_cmc_exchange_liquidity_context",
958
- "market_cmc_fear_greed_context",
959
- "market_cmc_index_context"
960
- ],
961
- derivatives: ["derivatives_market"],
962
- hyperliquidWhales: [
963
- "hyperliquid_whale_flow",
964
- "hyperliquid_whale_coverage_1m"
965
- ]
966
- };
967
- var verifyMarketContextSchema = async (source) => {
968
- if (verifiedMarketContextSchemas.has(source)) return;
969
- const tables = MARKET_CONTEXT_SCHEMA_TABLES[source];
970
- const result = await queryMarketContext(
971
- `
972
- SELECT table_name AS "tableName"
973
- FROM unnest($1::text[]) AS requested(table_name)
974
- WHERE to_regclass(requested.table_name) IS NULL
975
- `,
976
- [tables]
977
- );
978
- if (result.rows.length) {
979
- throw new Error(
980
- `Timescale ${source} schema is not prepared; missing: ${result.rows.map((row) => row.tableName).filter(Boolean).join(", ")}`
981
- );
982
- }
983
- verifiedMarketContextSchemas.add(source);
984
- };
985
- var prepareMarketContextSchemaForRead = async (source) => marketContextSchemaMode === "verify" ? verifyMarketContextSchema(source) : ensureMarketContextSchema(source);
986
- var ensureMarketContextSchemas = async (sources) => {
987
- for (const source of new Set(sources)) {
988
- await ensureMarketContextSchema(source);
989
- }
990
- };
991
- async function upsertDerivatives(rows) {
992
- if (!rows.length) return;
993
- await ensureDerivativesSchema();
994
- const pool = getPool();
995
- const cols = [
996
- "symbol",
997
- "interval",
998
- "ts",
999
- "open_interest",
1000
- "funding_rate",
1001
- "liq_long",
1002
- "liq_short",
1003
- "liq_total",
1004
- "source"
1005
- ];
1006
- const maxRows = Math.floor(65535 / cols.length);
1007
- if (rows.length > maxRows) {
1008
- for (let i = 0; i < rows.length; i += maxRows) {
1009
- await upsertDerivatives(rows.slice(i, i + maxRows));
1010
- }
1011
- return;
1012
- }
1013
- const valuesSql = rows.map(
1014
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1015
- ).join(",");
1016
- const flat = rows.flatMap((row) => [
1017
- row.symbol,
1018
- row.interval,
1019
- row.ts,
1020
- row.openInterest ?? null,
1021
- row.fundingRate ?? null,
1022
- row.liqLong ?? null,
1023
- row.liqShort ?? null,
1024
- row.liqTotal ?? null,
1025
- row.source ?? null
1026
- ]);
1027
- const sql = `
1028
- INSERT INTO derivatives_market (${cols.join(",")})
1029
- VALUES ${valuesSql}
1030
- ON CONFLICT (symbol, interval, ts) DO UPDATE SET
1031
- open_interest = COALESCE(EXCLUDED.open_interest, derivatives_market.open_interest),
1032
- funding_rate = COALESCE(EXCLUDED.funding_rate, derivatives_market.funding_rate),
1033
- liq_long = COALESCE(EXCLUDED.liq_long, derivatives_market.liq_long),
1034
- liq_short = COALESCE(EXCLUDED.liq_short, derivatives_market.liq_short),
1035
- liq_total = COALESCE(EXCLUDED.liq_total, derivatives_market.liq_total),
1036
- source = COALESCE(EXCLUDED.source, derivatives_market.source),
1037
- ingested_at = now()
1038
- `;
1039
- await pool.query(sql, flat);
1040
- }
1041
- async function getDerivativesRangeForSymbols(symbols, interval, startMs, endMs) {
1042
- if (!symbols.length)
1043
- return [];
1044
- await ensureDerivativesSchema();
1045
- const pool = getPool();
1046
- const sql = `
1047
- SELECT symbol, interval, ts, open_interest, funding_rate, liq_long, liq_short, liq_total
1048
- FROM derivatives_market
1049
- WHERE symbol = ANY($1)
1050
- AND interval = $2
1051
- AND ts >= to_timestamp($3/1000.0)
1052
- AND ts <= to_timestamp($4/1000.0)
1053
- ORDER BY symbol ASC, ts ASC
1054
- `;
1055
- const res = await pool.query(sql, [symbols, interval, startMs, endMs]);
1056
- return res.rows;
1057
- }
1058
- async function getDerivativesDataEdgesForSymbols(symbols, interval) {
1059
- const normalizedSymbols = [
1060
- ...new Set(
1061
- symbols.map(
1062
- (symbol) => String(symbol || "").trim().toUpperCase()
1063
- ).filter(Boolean)
1064
- )
1065
- ];
1066
- const edges = /* @__PURE__ */ new Map();
1067
- if (!normalizedSymbols.length) return edges;
1068
- await ensureDerivativesSchema();
1069
- const pool = getPool();
1070
- const sql = `
1071
- SELECT
1072
- symbol,
1073
- extract(epoch from MIN(ts))*1000 AS min,
1074
- extract(epoch from MAX(ts))*1000 AS max
1075
- FROM derivatives_market
1076
- WHERE symbol = ANY($1)
1077
- AND interval = $2
1078
- GROUP BY symbol
1079
- `;
1080
- const res = await pool.query(sql, [normalizedSymbols, interval]);
1081
- for (const row of res.rows) {
1082
- const min = Number(row.min);
1083
- const max = Number(row.max);
1084
- edges.set(String(row.symbol).toUpperCase(), {
1085
- min: Number.isFinite(min) ? min : void 0,
1086
- max: Number.isFinite(max) ? max : void 0
1087
- });
1088
- }
1089
- return edges;
1090
- }
1091
- async function getDerivativesBackfillCoverage(params) {
1092
- const normalizedSource = String(params.source || "").trim().toLowerCase();
1093
- const normalizedSymbols = [
1094
- ...new Set(
1095
- params.symbols.map(
1096
- (symbol) => String(symbol || "").trim().toUpperCase()
1097
- ).filter(Boolean)
1098
- )
1099
- ];
1100
- if (!normalizedSource || !normalizedSymbols.length) {
1101
- return [];
1102
- }
1103
- await ensureDerivativesSchema();
1104
- const pool = getPool();
1105
- const res = await pool.query(
1106
- `
1107
- SELECT
1108
- symbol,
1109
- interval,
1110
- extract(epoch from from_ts)*1000 AS from_ms,
1111
- extract(epoch from to_ts)*1000 AS to_ms,
1112
- rows_count
1113
- FROM derivatives_backfill_coverage
1114
- WHERE source = $1
1115
- AND symbol = ANY($2)
1116
- AND interval = $3
1117
- AND from_ts <= to_timestamp($5/1000.0)
1118
- AND to_ts >= to_timestamp($4/1000.0)
1119
- `,
1120
- [
1121
- normalizedSource,
1122
- normalizedSymbols,
1123
- params.interval,
1124
- params.fromMs,
1125
- params.toMs
1126
- ]
1127
- );
1128
- return res.rows.map((row) => ({
1129
- symbol: String(row.symbol).toUpperCase(),
1130
- interval: row.interval,
1131
- fromMs: Number(row.from_ms),
1132
- toMs: Number(row.to_ms),
1133
- rowsCount: Number(row.rows_count ?? 0)
1134
- }));
1135
- }
1136
- async function upsertDerivativesBackfillCoverage(rows) {
1137
- if (!rows.length) return;
1138
- await ensureDerivativesSchema();
1139
- const pool = getPool();
1140
- const cols = [
1141
- "source",
1142
- "symbol",
1143
- "interval",
1144
- "from_ts",
1145
- "to_ts",
1146
- "rows_count"
1147
- ];
1148
- const valuesSql = rows.map(
1149
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1150
- ).join(",");
1151
- const flat = rows.flatMap((row) => [
1152
- String(row.source || "").trim().toLowerCase(),
1153
- String(row.symbol || "").trim().toUpperCase(),
1154
- row.interval,
1155
- new Date(row.fromMs),
1156
- new Date(row.toMs),
1157
- Math.max(0, Math.trunc(row.rowsCount))
1158
- ]);
1159
- await pool.query(
1160
- `
1161
- INSERT INTO derivatives_backfill_coverage (${cols.join(",")})
1162
- VALUES ${valuesSql}
1163
- ON CONFLICT (source, symbol, interval, from_ts, to_ts) DO UPDATE SET
1164
- rows_count = EXCLUDED.rows_count,
1165
- checked_at = now()
1166
- `,
1167
- flat
1168
- );
1169
- }
1170
- async function getDerivativesMetricCoverage(params) {
1171
- const normalizedSource = String(params.source || "").trim().toLowerCase();
1172
- const normalizedSymbols = [
1173
- ...new Set(
1174
- params.symbols.map(
1175
- (symbol) => String(symbol || "").trim().toUpperCase()
1176
- ).filter(Boolean)
1177
- )
1178
- ];
1179
- if (!normalizedSource || !normalizedSymbols.length) {
1180
- return [];
1181
- }
1182
- await ensureDerivativesSchema();
1183
- const pool = getPool();
1184
- const res = await pool.query(
1185
- `
1186
- SELECT
1187
- symbol,
1188
- interval,
1189
- extract(epoch from from_ts)*1000 AS from_ms,
1190
- extract(epoch from to_ts)*1000 AS to_ms,
1191
- event_rows_count,
1192
- zero_rows_count
1193
- FROM derivatives_metric_coverage
1194
- WHERE source = $1
1195
- AND metric = $2
1196
- AND symbol = ANY($3)
1197
- AND interval = $4
1198
- AND from_ts <= to_timestamp($6/1000.0)
1199
- AND to_ts >= to_timestamp($5/1000.0)
1200
- `,
1201
- [
1202
- normalizedSource,
1203
- params.metric,
1204
- normalizedSymbols,
1205
- params.interval,
1206
- params.fromMs,
1207
- params.toMs
1208
- ]
1209
- );
1210
- return res.rows.map((row) => ({
1211
- symbol: String(row.symbol).toUpperCase(),
1212
- interval: row.interval,
1213
- fromMs: Number(row.from_ms),
1214
- toMs: Number(row.to_ms),
1215
- eventRowsCount: Number(row.event_rows_count ?? 0),
1216
- zeroRowsCount: Number(row.zero_rows_count ?? 0)
1217
- }));
1218
- }
1219
- async function applyDerivativesMetricCoverage(rows) {
1220
- if (!rows.length)
1221
- return [];
1222
- await ensureDerivativesSchema();
1223
- const pool = getPool();
1224
- const client = await pool.connect();
1225
- const results = [];
1226
- try {
1227
- await client.query("BEGIN");
1228
- for (const row of rows) {
1229
- const source = String(row.source || "").trim().toLowerCase();
1230
- const symbol = String(row.symbol || "").trim().toUpperCase();
1231
- const fromMs = Math.trunc(row.fromMs);
1232
- const toMs = Math.trunc(row.toMs);
1233
- if (!source || !symbol || fromMs > toMs) continue;
1234
- await client.query(
1235
- `
1236
- UPDATE derivatives_market
1237
- SET
1238
- liq_long = 0,
1239
- liq_short = 0,
1240
- liq_total = 0,
1241
- ingested_at = now()
1242
- WHERE symbol = $1
1243
- AND interval = $2
1244
- AND ts >= to_timestamp($3/1000.0)
1245
- AND ts <= to_timestamp($4/1000.0)
1246
- AND liq_long IS NULL
1247
- AND liq_short IS NULL
1248
- AND liq_total IS NULL
1249
- `,
1250
- [symbol, row.interval, fromMs, toMs]
1251
- );
1252
- const zeroCountResult = await client.query(
1253
- `
1254
- SELECT COUNT(*)::integer AS count
1255
- FROM derivatives_market
1256
- WHERE symbol = $1
1257
- AND interval = $2
1258
- AND ts >= to_timestamp($3/1000.0)
1259
- AND ts <= to_timestamp($4/1000.0)
1260
- AND liq_long = 0
1261
- AND liq_short = 0
1262
- AND liq_total = 0
1263
- `,
1264
- [symbol, row.interval, fromMs, toMs]
1265
- );
1266
- const zeroRowsCount = Math.max(
1267
- 0,
1268
- Number(zeroCountResult.rows[0]?.count ?? 0)
1269
- );
1270
- await client.query(
1271
- `
1272
- INSERT INTO derivatives_metric_coverage (
1273
- source,
1274
- metric,
1275
- symbol,
1276
- interval,
1277
- from_ts,
1278
- to_ts,
1279
- event_rows_count,
1280
- zero_rows_count
1281
- )
1282
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1283
- ON CONFLICT (source, metric, symbol, interval, from_ts, to_ts)
1284
- DO UPDATE SET
1285
- event_rows_count = EXCLUDED.event_rows_count,
1286
- zero_rows_count = EXCLUDED.zero_rows_count,
1287
- checked_at = now()
1288
- `,
1289
- [
1290
- source,
1291
- row.metric,
1292
- symbol,
1293
- row.interval,
1294
- new Date(fromMs),
1295
- new Date(toMs),
1296
- Math.max(0, Math.trunc(row.eventRowsCount)),
1297
- zeroRowsCount
1298
- ]
1299
- );
1300
- results.push({ symbol, zeroRowsCount });
1301
- }
1302
- await client.query("COMMIT");
1303
- return results;
1304
- } catch (error) {
1305
- await client.query("ROLLBACK");
1306
- throw error;
1307
- } finally {
1308
- client.release();
1309
- }
1310
- }
1311
- async function getDerivativesWindow(params) {
1312
- const { symbol, intervals, endMs, lookbackMs } = params;
1313
- const normalizedSymbol = String(symbol || "").trim().toUpperCase();
1314
- const normalizedIntervals = [...new Set(intervals)].filter(Boolean);
1315
- if (!normalizedSymbol || !normalizedIntervals.length) {
1316
- return {};
1317
- }
1318
- await prepareMarketContextSchemaForRead("derivatives");
1319
- const startMs = endMs - Math.max(0, lookbackMs);
1320
- const sql = `
1321
- SELECT symbol, interval, ts, open_interest, funding_rate, liq_long, liq_short, liq_total, source
1322
- FROM derivatives_market
1323
- WHERE symbol = $1
1324
- AND interval = ANY($2)
1325
- AND ts >= to_timestamp($3/1000.0)
1326
- AND ts <= to_timestamp($4/1000.0)
1327
- ORDER BY interval ASC, ts ASC
1328
- `;
1329
- const res = await queryMarketContext(
1330
- sql,
1331
- [normalizedSymbol, normalizedIntervals, startMs, endMs],
1332
- params
1333
- );
1334
- const rowsByInterval = {};
1335
- for (const row of res.rows) {
1336
- const interval = row.interval;
1337
- rowsByInterval[interval] ??= [];
1338
- rowsByInterval[interval]?.push({
1339
- symbol: row.symbol,
1340
- interval,
1341
- ts: row.ts,
1342
- openInterest: row.open_interest,
1343
- fundingRate: row.funding_rate,
1344
- liqLong: row.liq_long,
1345
- liqShort: row.liq_short,
1346
- liqTotal: row.liq_total,
1347
- source: row.source
1348
- });
1349
- }
1350
- return rowsByInterval;
1351
- }
1352
- async function getDerivativesSummary(hours = 24, limit = 500, symbols) {
1353
- await ensureDerivativesSchema();
1354
- const pool = getPool();
1355
- const cappedHours = Math.max(1, Math.min(24 * 90, hours));
1356
- const cappedLimit = Math.max(10, Math.min(1e3, limit));
1357
- const normalizedSymbols = Array.isArray(symbols) ? [...new Set(symbols.map(normalizeCandleSymbol).filter(Boolean))] : [];
1358
- const symbolsFilterSql = normalizedSymbols.length ? "AND symbol = ANY($3)" : "";
1359
- const summaryQ = await pool.query(
1360
- `
1361
- WITH filtered AS (
1362
- SELECT
1363
- symbol,
1364
- interval,
1365
- ts,
1366
- open_interest,
1367
- funding_rate,
1368
- liq_long,
1369
- liq_short,
1370
- liq_total
1371
- FROM derivatives_market
1372
- WHERE ts >= now() - ($1 || ' hours')::interval
1373
- ${symbolsFilterSql}
1374
- ),
1375
- latest AS (
1376
- SELECT DISTINCT ON (symbol, interval)
1377
- symbol,
1378
- interval,
1379
- ts AS last_ts,
1380
- open_interest AS latest_open_interest,
1381
- funding_rate AS latest_funding_rate
1382
- FROM filtered
1383
- ORDER BY symbol ASC, interval ASC, ts DESC
1384
- ),
1385
- first AS (
1386
- SELECT DISTINCT ON (symbol, interval)
1387
- symbol,
1388
- interval,
1389
- ts AS first_ts,
1390
- open_interest AS first_open_interest,
1391
- funding_rate AS first_funding_rate
1392
- FROM filtered
1393
- ORDER BY symbol ASC, interval ASC, ts ASC
1394
- ),
1395
- aggregated AS (
1396
- SELECT
1397
- symbol,
1398
- interval,
1399
- COUNT(*)::int AS points,
1400
- SUM(COALESCE(liq_long, 0)) AS sum_liq_long,
1401
- SUM(COALESCE(liq_short, 0)) AS sum_liq_short,
1402
- SUM(COALESCE(liq_total, 0)) AS sum_liq_total
1403
- FROM filtered
1404
- GROUP BY symbol, interval
1405
- )
1406
- SELECT
1407
- aggregated.symbol,
1408
- aggregated.interval,
1409
- aggregated.points,
1410
- latest.last_ts,
1411
- first.first_ts,
1412
- latest.latest_open_interest,
1413
- first.first_open_interest,
1414
- latest.latest_funding_rate,
1415
- first.first_funding_rate,
1416
- aggregated.sum_liq_long,
1417
- aggregated.sum_liq_short,
1418
- aggregated.sum_liq_total
1419
- FROM aggregated
1420
- JOIN latest
1421
- ON latest.symbol = aggregated.symbol
1422
- AND latest.interval = aggregated.interval
1423
- JOIN first
1424
- ON first.symbol = aggregated.symbol
1425
- AND first.interval = aggregated.interval
1426
- ORDER BY aggregated.sum_liq_total DESC, aggregated.symbol ASC
1427
- LIMIT $2
1428
- `,
1429
- normalizedSymbols.length ? [String(cappedHours), cappedLimit, normalizedSymbols] : [String(cappedHours), cappedLimit]
1430
- );
1431
- const items = summaryQ.rows.map((row) => {
1432
- const latestOpenInterest = row.latest_open_interest == null ? null : Number(row.latest_open_interest);
1433
- const firstOpenInterest = row.first_open_interest == null ? null : Number(row.first_open_interest);
1434
- const latestFundingRate = row.latest_funding_rate == null ? null : Number(row.latest_funding_rate);
1435
- const firstFundingRate = row.first_funding_rate == null ? null : Number(row.first_funding_rate);
1436
- const oiChange = latestOpenInterest != null && firstOpenInterest != null ? latestOpenInterest - firstOpenInterest : null;
1437
- const oiChangePct = oiChange != null && firstOpenInterest != null && Number.isFinite(firstOpenInterest) && Math.abs(firstOpenInterest) > 0 ? oiChange / Math.abs(firstOpenInterest) * 100 : null;
1438
- const fundingChange = latestFundingRate != null && firstFundingRate != null ? latestFundingRate - firstFundingRate : null;
1439
- return {
1440
- symbol: row.symbol,
1441
- interval: row.interval,
1442
- points: Number(row.points || 0),
1443
- last_ts: row.last_ts,
1444
- first_ts: row.first_ts,
1445
- latest_open_interest: latestOpenInterest,
1446
- first_open_interest: firstOpenInterest,
1447
- oi_change: oiChange,
1448
- oi_change_pct: oiChangePct,
1449
- latest_funding_rate: latestFundingRate,
1450
- first_funding_rate: firstFundingRate,
1451
- funding_change: fundingChange,
1452
- sum_liq_long: row.sum_liq_long == null ? null : Number(row.sum_liq_long),
1453
- sum_liq_short: row.sum_liq_short == null ? null : Number(row.sum_liq_short),
1454
- sum_liq_total: row.sum_liq_total == null ? null : Number(row.sum_liq_total)
1455
- };
1456
- });
1457
- return {
1458
- hours: cappedHours,
1459
- items
1460
- };
1461
- }
1462
- async function upsertSpreadRows(rows) {
1463
- if (!rows.length) return;
1464
- await ensureSpreadSchema();
1465
- const pool = getPool();
1466
- const cols = [
1467
- "symbol",
1468
- "interval",
1469
- "ts",
1470
- "binance_price",
1471
- "coinbase_price",
1472
- "spread",
1473
- "source"
1474
- ];
1475
- const maxRows = Math.floor(65535 / cols.length);
1476
- if (rows.length > maxRows) {
1477
- for (let i = 0; i < rows.length; i += maxRows) {
1478
- await upsertSpreadRows(rows.slice(i, i + maxRows));
1479
- }
1480
- return;
1481
- }
1482
- const valuesSql = rows.map(
1483
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1484
- ).join(",");
1485
- const flat = rows.flatMap((row) => [
1486
- row.symbol,
1487
- row.interval,
1488
- row.ts,
1489
- row.binancePrice ?? null,
1490
- row.coinbasePrice ?? null,
1491
- row.spread ?? null,
1492
- row.source ?? null
1493
- ]);
1494
- const sql = `
1495
- INSERT INTO market_spread (${cols.join(",")})
1496
- VALUES ${valuesSql}
1497
- ON CONFLICT (symbol, interval, ts) DO UPDATE SET
1498
- binance_price = COALESCE(EXCLUDED.binance_price, market_spread.binance_price),
1499
- coinbase_price = COALESCE(EXCLUDED.coinbase_price, market_spread.coinbase_price),
1500
- spread = COALESCE(EXCLUDED.spread, market_spread.spread),
1501
- source = COALESCE(EXCLUDED.source, market_spread.source),
1502
- ingested_at = now()
1503
- `;
1504
- await pool.query(sql, flat);
1505
- }
1506
- async function upsertMarketTradeFlowRows(rows) {
1507
- if (!rows.length) return;
1508
- await ensureBinanceMarketSchema();
1509
- const pool = getPool();
1510
- const cols = [
1511
- "symbol",
1512
- "interval",
1513
- "ts",
1514
- "trades",
1515
- "buy_base_volume",
1516
- "sell_base_volume",
1517
- "buy_quote_volume",
1518
- "sell_quote_volume",
1519
- "net_base_delta",
1520
- "net_quote_delta",
1521
- "buy_pressure_pct",
1522
- "source"
1523
- ];
1524
- const maxRows = getSafeBulkInsertRows(cols.length);
1525
- if (rows.length > maxRows) {
1526
- for (let i = 0; i < rows.length; i += maxRows) {
1527
- await upsertMarketTradeFlowRows(rows.slice(i, i + maxRows));
1528
- }
1529
- return;
1530
- }
1531
- const valuesSql = rows.map(
1532
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1533
- ).join(",");
1534
- const flat = rows.flatMap((row) => [
1535
- row.symbol,
1536
- row.interval,
1537
- row.ts,
1538
- row.trades,
1539
- row.buyBaseVolume ?? null,
1540
- row.sellBaseVolume ?? null,
1541
- row.buyQuoteVolume ?? null,
1542
- row.sellQuoteVolume ?? null,
1543
- row.netBaseDelta ?? null,
1544
- row.netQuoteDelta ?? null,
1545
- row.buyPressurePct ?? null,
1546
- row.source ?? null
1547
- ]);
1548
- await pool.query(
1549
- `
1550
- INSERT INTO market_trade_flow (${cols.join(",")})
1551
- VALUES ${valuesSql}
1552
- ON CONFLICT (symbol, interval, ts) DO UPDATE SET
1553
- trades = EXCLUDED.trades,
1554
- buy_base_volume = COALESCE(EXCLUDED.buy_base_volume, market_trade_flow.buy_base_volume),
1555
- sell_base_volume = COALESCE(EXCLUDED.sell_base_volume, market_trade_flow.sell_base_volume),
1556
- buy_quote_volume = COALESCE(EXCLUDED.buy_quote_volume, market_trade_flow.buy_quote_volume),
1557
- sell_quote_volume = COALESCE(EXCLUDED.sell_quote_volume, market_trade_flow.sell_quote_volume),
1558
- net_base_delta = COALESCE(EXCLUDED.net_base_delta, market_trade_flow.net_base_delta),
1559
- net_quote_delta = COALESCE(EXCLUDED.net_quote_delta, market_trade_flow.net_quote_delta),
1560
- buy_pressure_pct = COALESCE(EXCLUDED.buy_pressure_pct, market_trade_flow.buy_pressure_pct),
1561
- source = COALESCE(EXCLUDED.source, market_trade_flow.source),
1562
- ingested_at = now()
1563
- `,
1564
- flat
1565
- );
1566
- }
1567
- async function upsertHyperliquidWhaleTradeEvents(rows) {
1568
- if (!rows.length) return;
1569
- await ensureHyperliquidWhaleSchema();
1570
- const cols = [
1571
- "symbol",
1572
- "ts",
1573
- "tid",
1574
- "price",
1575
- "size",
1576
- "notional_usd",
1577
- "buyer_address",
1578
- "seller_address",
1579
- "buyer_tracked",
1580
- "seller_tracked",
1581
- "buyer_start_position",
1582
- "buyer_end_position",
1583
- "buyer_position_action",
1584
- "buyer_closed_pnl",
1585
- "buyer_liquidation",
1586
- "seller_start_position",
1587
- "seller_end_position",
1588
- "seller_position_action",
1589
- "seller_closed_pnl",
1590
- "seller_liquidation",
1591
- "universe_fingerprint",
1592
- "whale_registry_fingerprint",
1593
- "source"
1594
- ];
1595
- const maxRows = getSafeBulkInsertRows(cols.length);
1596
- if (rows.length > maxRows) {
1597
- for (let index = 0; index < rows.length; index += maxRows) {
1598
- await upsertHyperliquidWhaleTradeEvents(
1599
- rows.slice(index, index + maxRows)
1600
- );
1601
- }
1602
- return;
1603
- }
1604
- const valuesSql = rows.map(
1605
- (_, rowIndex) => `(${cols.map((__, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(",")})`
1606
- ).join(",");
1607
- const flat = rows.flatMap((row) => [
1608
- row.symbol,
1609
- row.ts,
1610
- row.tid,
1611
- row.price,
1612
- row.size,
1613
- row.notionalUsd,
1614
- row.buyerAddress ?? null,
1615
- row.sellerAddress ?? null,
1616
- row.buyerTracked,
1617
- row.sellerTracked,
1618
- row.buyerStartPosition ?? null,
1619
- row.buyerEndPosition ?? null,
1620
- row.buyerPositionAction ?? null,
1621
- row.buyerClosedPnl ?? null,
1622
- row.buyerLiquidation ?? null,
1623
- row.sellerStartPosition ?? null,
1624
- row.sellerEndPosition ?? null,
1625
- row.sellerPositionAction ?? null,
1626
- row.sellerClosedPnl ?? null,
1627
- row.sellerLiquidation ?? null,
1628
- row.universeFingerprint,
1629
- row.whaleRegistryFingerprint,
1630
- row.source ?? null
1631
- ]);
1632
- await getPool().query(
1633
- `
1634
- INSERT INTO hyperliquid_whale_trade_events (${cols.join(",")})
1635
- VALUES ${valuesSql}
1636
- ON CONFLICT (
1637
- universe_fingerprint,
1638
- whale_registry_fingerprint,
1639
- symbol,
1640
- ts,
1641
- tid
1642
- ) DO UPDATE SET
1643
- buyer_address = COALESCE(
1644
- hyperliquid_whale_trade_events.buyer_address,
1645
- EXCLUDED.buyer_address
1646
- ),
1647
- seller_address = COALESCE(
1648
- hyperliquid_whale_trade_events.seller_address,
1649
- EXCLUDED.seller_address
1650
- ),
1651
- buyer_tracked = hyperliquid_whale_trade_events.buyer_tracked OR EXCLUDED.buyer_tracked,
1652
- seller_tracked = hyperliquid_whale_trade_events.seller_tracked OR EXCLUDED.seller_tracked,
1653
- buyer_start_position = COALESCE(
1654
- hyperliquid_whale_trade_events.buyer_start_position,
1655
- EXCLUDED.buyer_start_position
1656
- ),
1657
- buyer_end_position = COALESCE(
1658
- hyperliquid_whale_trade_events.buyer_end_position,
1659
- EXCLUDED.buyer_end_position
1660
- ),
1661
- buyer_position_action = COALESCE(
1662
- hyperliquid_whale_trade_events.buyer_position_action,
1663
- EXCLUDED.buyer_position_action
1664
- ),
1665
- buyer_closed_pnl = COALESCE(
1666
- hyperliquid_whale_trade_events.buyer_closed_pnl,
1667
- EXCLUDED.buyer_closed_pnl
1668
- ),
1669
- buyer_liquidation = COALESCE(
1670
- hyperliquid_whale_trade_events.buyer_liquidation,
1671
- EXCLUDED.buyer_liquidation
1672
- ),
1673
- seller_start_position = COALESCE(
1674
- hyperliquid_whale_trade_events.seller_start_position,
1675
- EXCLUDED.seller_start_position
1676
- ),
1677
- seller_end_position = COALESCE(
1678
- hyperliquid_whale_trade_events.seller_end_position,
1679
- EXCLUDED.seller_end_position
1680
- ),
1681
- seller_position_action = COALESCE(
1682
- hyperliquid_whale_trade_events.seller_position_action,
1683
- EXCLUDED.seller_position_action
1684
- ),
1685
- seller_closed_pnl = COALESCE(
1686
- hyperliquid_whale_trade_events.seller_closed_pnl,
1687
- EXCLUDED.seller_closed_pnl
1688
- ),
1689
- seller_liquidation = COALESCE(
1690
- hyperliquid_whale_trade_events.seller_liquidation,
1691
- EXCLUDED.seller_liquidation
1692
- ),
1693
- source = EXCLUDED.source,
1694
- ingested_at = now()
1695
- `,
1696
- flat
1697
- );
1698
- }
1699
- async function upsertHyperliquidWhaleFlowRows(rows) {
1700
- if (!rows.length) return;
1701
- await ensureHyperliquidWhaleSchema();
1702
- const cols = [
1703
- "symbol",
1704
- "interval",
1705
- "ts",
1706
- "trades",
1707
- "whale_sides",
1708
- "unique_whales",
1709
- "whale_addresses",
1710
- "buy_notional_usd",
1711
- "sell_notional_usd",
1712
- "net_notional_usd",
1713
- "buy_share_pct",
1714
- "position_aware_whale_sides",
1715
- "long_entry_whale_addresses",
1716
- "short_entry_whale_addresses",
1717
- "long_exit_whale_addresses",
1718
- "short_exit_whale_addresses",
1719
- "long_entry_notional_usd",
1720
- "short_entry_notional_usd",
1721
- "long_exit_notional_usd",
1722
- "short_exit_notional_usd",
1723
- "entry_net_notional_usd",
1724
- "entry_long_share_pct",
1725
- "universe_fingerprint",
1726
- "whale_registry_fingerprint",
1727
- "source"
1728
- ];
1729
- const maxRows = getSafeBulkInsertRows(cols.length);
1730
- if (rows.length > maxRows) {
1731
- for (let index = 0; index < rows.length; index += maxRows) {
1732
- await upsertHyperliquidWhaleFlowRows(rows.slice(index, index + maxRows));
1733
- }
1734
- return;
1735
- }
1736
- const valuesSql = rows.map(
1737
- (_, rowIndex) => `(${cols.map((__, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(",")})`
1738
- ).join(",");
1739
- const flat = rows.flatMap((row) => [
1740
- row.symbol,
1741
- row.interval,
1742
- row.ts,
1743
- row.trades,
1744
- row.whaleSides,
1745
- row.uniqueWhales,
1746
- row.whaleAddresses ?? [],
1747
- row.buyNotionalUsd,
1748
- row.sellNotionalUsd,
1749
- row.netNotionalUsd,
1750
- row.buySharePct ?? null,
1751
- row.positionAwareWhaleSides,
1752
- row.longEntryWhaleAddresses ?? [],
1753
- row.shortEntryWhaleAddresses ?? [],
1754
- row.longExitWhaleAddresses ?? [],
1755
- row.shortExitWhaleAddresses ?? [],
1756
- row.longEntryNotionalUsd,
1757
- row.shortEntryNotionalUsd,
1758
- row.longExitNotionalUsd,
1759
- row.shortExitNotionalUsd,
1760
- row.entryNetNotionalUsd,
1761
- row.entryLongSharePct ?? null,
1762
- row.universeFingerprint,
1763
- row.whaleRegistryFingerprint,
1764
- row.source ?? null
1765
- ]);
1766
- await getPool().query(
1767
- `
1768
- INSERT INTO hyperliquid_whale_flow (${cols.join(",")})
1769
- VALUES ${valuesSql}
1770
- ON CONFLICT (
1771
- universe_fingerprint,
1772
- whale_registry_fingerprint,
1773
- symbol,
1774
- interval,
1775
- ts
1776
- ) DO UPDATE SET
1777
- trades = EXCLUDED.trades,
1778
- whale_sides = EXCLUDED.whale_sides,
1779
- unique_whales = EXCLUDED.unique_whales,
1780
- whale_addresses = EXCLUDED.whale_addresses,
1781
- buy_notional_usd = EXCLUDED.buy_notional_usd,
1782
- sell_notional_usd = EXCLUDED.sell_notional_usd,
1783
- net_notional_usd = EXCLUDED.net_notional_usd,
1784
- buy_share_pct = EXCLUDED.buy_share_pct,
1785
- position_aware_whale_sides = EXCLUDED.position_aware_whale_sides,
1786
- long_entry_whale_addresses = EXCLUDED.long_entry_whale_addresses,
1787
- short_entry_whale_addresses = EXCLUDED.short_entry_whale_addresses,
1788
- long_exit_whale_addresses = EXCLUDED.long_exit_whale_addresses,
1789
- short_exit_whale_addresses = EXCLUDED.short_exit_whale_addresses,
1790
- long_entry_notional_usd = EXCLUDED.long_entry_notional_usd,
1791
- short_entry_notional_usd = EXCLUDED.short_entry_notional_usd,
1792
- long_exit_notional_usd = EXCLUDED.long_exit_notional_usd,
1793
- short_exit_notional_usd = EXCLUDED.short_exit_notional_usd,
1794
- entry_net_notional_usd = EXCLUDED.entry_net_notional_usd,
1795
- entry_long_share_pct = EXCLUDED.entry_long_share_pct,
1796
- source = EXCLUDED.source,
1797
- ingested_at = now()
1798
- `,
1799
- flat
1800
- );
1801
- }
1802
- async function rebuildHyperliquidWhaleFlowRows(params) {
1803
- await ensureHyperliquidWhaleSchema();
1804
- const client = await getPool().connect();
1805
- try {
1806
- await client.query("BEGIN");
1807
- const result = await client.query(
1808
- `
1809
- WITH source_events AS (
1810
- SELECT *
1811
- FROM hyperliquid_whale_trade_events
1812
- WHERE universe_fingerprint = $1
1813
- AND whale_registry_fingerprint = $2
1814
- AND ts >= to_timestamp($3/1000.0)
1815
- AND ts < to_timestamp($4/1000.0)
1816
- ), metrics AS (
1817
- SELECT
1818
- symbol,
1819
- date_trunc('minute', ts) AS bucket_ts,
1820
- COUNT(*)::int AS trades,
1821
- SUM(buyer_tracked::int + seller_tracked::int)::int AS whale_sides,
1822
- SUM(CASE WHEN buyer_tracked THEN notional_usd ELSE 0 END) AS buy_notional_usd,
1823
- SUM(CASE WHEN seller_tracked THEN notional_usd ELSE 0 END) AS sell_notional_usd
1824
- FROM source_events
1825
- GROUP BY symbol, date_trunc('minute', ts)
1826
- ), position_legs AS (
1827
- SELECT
1828
- symbol,
1829
- ts,
1830
- price,
1831
- buyer_address AS whale_address,
1832
- buyer_start_position AS start_position,
1833
- buyer_end_position AS end_position
1834
- FROM source_events
1835
- WHERE buyer_tracked
1836
- AND buyer_address IS NOT NULL
1837
- AND buyer_start_position IS NOT NULL
1838
- AND buyer_end_position IS NOT NULL
1839
- UNION ALL
1840
- SELECT
1841
- symbol,
1842
- ts,
1843
- price,
1844
- seller_address AS whale_address,
1845
- seller_start_position AS start_position,
1846
- seller_end_position AS end_position
1847
- FROM source_events
1848
- WHERE seller_tracked
1849
- AND seller_address IS NOT NULL
1850
- AND seller_start_position IS NOT NULL
1851
- AND seller_end_position IS NOT NULL
1852
- ), classified_legs AS (
1853
- SELECT
1854
- *,
1855
- GREATEST(
1856
- GREATEST(end_position, 0) - GREATEST(start_position, 0),
1857
- 0
1858
- ) AS long_entry_size,
1859
- GREATEST(
1860
- GREATEST(-end_position, 0) - GREATEST(-start_position, 0),
1861
- 0
1862
- ) AS short_entry_size,
1863
- GREATEST(
1864
- GREATEST(start_position, 0) - GREATEST(end_position, 0),
1865
- 0
1866
- ) AS long_exit_size,
1867
- GREATEST(
1868
- GREATEST(-start_position, 0) - GREATEST(-end_position, 0),
1869
- 0
1870
- ) AS short_exit_size
1871
- FROM position_legs
1872
- ), position_metrics AS (
1873
- SELECT
1874
- symbol,
1875
- date_trunc('minute', ts) AS bucket_ts,
1876
- COUNT(*)::int AS position_aware_whale_sides,
1877
- SUM(long_entry_size * price) AS long_entry_notional_usd,
1878
- SUM(short_entry_size * price) AS short_entry_notional_usd,
1879
- SUM(long_exit_size * price) AS long_exit_notional_usd,
1880
- SUM(short_exit_size * price) AS short_exit_notional_usd,
1881
- COALESCE(
1882
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1883
- FILTER (WHERE long_entry_size > 0),
1884
- '{}'
1885
- ) AS long_entry_whale_addresses,
1886
- COALESCE(
1887
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1888
- FILTER (WHERE short_entry_size > 0),
1889
- '{}'
1890
- ) AS short_entry_whale_addresses,
1891
- COALESCE(
1892
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1893
- FILTER (WHERE long_exit_size > 0),
1894
- '{}'
1895
- ) AS long_exit_whale_addresses,
1896
- COALESCE(
1897
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1898
- FILTER (WHERE short_exit_size > 0),
1899
- '{}'
1900
- ) AS short_exit_whale_addresses
1901
- FROM classified_legs
1902
- GROUP BY symbol, date_trunc('minute', ts)
1903
- ), addresses AS (
1904
- SELECT
1905
- symbol,
1906
- date_trunc('minute', ts) AS bucket_ts,
1907
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address) AS whale_addresses
1908
- FROM source_events
1909
- CROSS JOIN LATERAL UNNEST(ARRAY[
1910
- CASE WHEN buyer_tracked THEN buyer_address END,
1911
- CASE WHEN seller_tracked THEN seller_address END
1912
- ]) AS expanded(whale_address)
1913
- WHERE whale_address IS NOT NULL
1914
- GROUP BY symbol, date_trunc('minute', ts)
1915
- )
1916
- INSERT INTO hyperliquid_whale_flow (
1917
- symbol,
1918
- interval,
1919
- ts,
1920
- trades,
1921
- whale_sides,
1922
- unique_whales,
1923
- whale_addresses,
1924
- buy_notional_usd,
1925
- sell_notional_usd,
1926
- net_notional_usd,
1927
- buy_share_pct,
1928
- position_aware_whale_sides,
1929
- long_entry_whale_addresses,
1930
- short_entry_whale_addresses,
1931
- long_exit_whale_addresses,
1932
- short_exit_whale_addresses,
1933
- long_entry_notional_usd,
1934
- short_entry_notional_usd,
1935
- long_exit_notional_usd,
1936
- short_exit_notional_usd,
1937
- entry_net_notional_usd,
1938
- entry_long_share_pct,
1939
- universe_fingerprint,
1940
- whale_registry_fingerprint,
1941
- source
1942
- )
1943
- SELECT
1944
- metrics.symbol,
1945
- '1m',
1946
- metrics.bucket_ts,
1947
- metrics.trades,
1948
- metrics.whale_sides,
1949
- COALESCE(CARDINALITY(addresses.whale_addresses), 0),
1950
- COALESCE(addresses.whale_addresses, '{}'),
1951
- metrics.buy_notional_usd,
1952
- metrics.sell_notional_usd,
1953
- metrics.buy_notional_usd - metrics.sell_notional_usd,
1954
- CASE
1955
- WHEN metrics.buy_notional_usd + metrics.sell_notional_usd > 0
1956
- THEN metrics.buy_notional_usd /
1957
- (metrics.buy_notional_usd + metrics.sell_notional_usd)
1958
- ELSE NULL
1959
- END,
1960
- COALESCE(position_metrics.position_aware_whale_sides, 0),
1961
- COALESCE(position_metrics.long_entry_whale_addresses, '{}'),
1962
- COALESCE(position_metrics.short_entry_whale_addresses, '{}'),
1963
- COALESCE(position_metrics.long_exit_whale_addresses, '{}'),
1964
- COALESCE(position_metrics.short_exit_whale_addresses, '{}'),
1965
- COALESCE(position_metrics.long_entry_notional_usd, 0),
1966
- COALESCE(position_metrics.short_entry_notional_usd, 0),
1967
- COALESCE(position_metrics.long_exit_notional_usd, 0),
1968
- COALESCE(position_metrics.short_exit_notional_usd, 0),
1969
- COALESCE(position_metrics.long_entry_notional_usd, 0) -
1970
- COALESCE(position_metrics.short_entry_notional_usd, 0),
1971
- CASE
1972
- WHEN COALESCE(position_metrics.long_entry_notional_usd, 0) +
1973
- COALESCE(position_metrics.short_entry_notional_usd, 0) > 0
1974
- THEN COALESCE(position_metrics.long_entry_notional_usd, 0) /
1975
- (
1976
- COALESCE(position_metrics.long_entry_notional_usd, 0) +
1977
- COALESCE(position_metrics.short_entry_notional_usd, 0)
1978
- )
1979
- ELSE NULL
1980
- END,
1981
- $1,
1982
- $2,
1983
- CASE
1984
- WHEN COALESCE(position_metrics.position_aware_whale_sides, 0) > 0
1985
- THEN 'hyperliquid_user_fills'
1986
- ELSE 'hyperliquid_trades'
1987
- END
1988
- FROM metrics
1989
- LEFT JOIN addresses USING (symbol, bucket_ts)
1990
- LEFT JOIN position_metrics USING (symbol, bucket_ts)
1991
- ON CONFLICT (
1992
- universe_fingerprint,
1993
- whale_registry_fingerprint,
1994
- symbol,
1995
- interval,
1996
- ts
1997
- ) DO UPDATE SET
1998
- trades = EXCLUDED.trades,
1999
- whale_sides = EXCLUDED.whale_sides,
2000
- unique_whales = EXCLUDED.unique_whales,
2001
- whale_addresses = EXCLUDED.whale_addresses,
2002
- buy_notional_usd = EXCLUDED.buy_notional_usd,
2003
- sell_notional_usd = EXCLUDED.sell_notional_usd,
2004
- net_notional_usd = EXCLUDED.net_notional_usd,
2005
- buy_share_pct = EXCLUDED.buy_share_pct,
2006
- position_aware_whale_sides = EXCLUDED.position_aware_whale_sides,
2007
- long_entry_whale_addresses = EXCLUDED.long_entry_whale_addresses,
2008
- short_entry_whale_addresses = EXCLUDED.short_entry_whale_addresses,
2009
- long_exit_whale_addresses = EXCLUDED.long_exit_whale_addresses,
2010
- short_exit_whale_addresses = EXCLUDED.short_exit_whale_addresses,
2011
- long_entry_notional_usd = EXCLUDED.long_entry_notional_usd,
2012
- short_entry_notional_usd = EXCLUDED.short_entry_notional_usd,
2013
- long_exit_notional_usd = EXCLUDED.long_exit_notional_usd,
2014
- short_exit_notional_usd = EXCLUDED.short_exit_notional_usd,
2015
- entry_net_notional_usd = EXCLUDED.entry_net_notional_usd,
2016
- entry_long_share_pct = EXCLUDED.entry_long_share_pct,
2017
- source = EXCLUDED.source,
2018
- ingested_at = now()
2019
- RETURNING 1
2020
- `,
2021
- [
2022
- params.universeFingerprint,
2023
- params.whaleRegistryFingerprint,
2024
- params.fromMs,
2025
- params.toMs
2026
- ]
2027
- );
2028
- if (params.deleteEventsBeforeMs != null) {
2029
- await client.query(
2030
- `
2031
- DELETE FROM hyperliquid_whale_trade_events
2032
- WHERE universe_fingerprint = $1
2033
- AND whale_registry_fingerprint = $2
2034
- AND ts < to_timestamp($3/1000.0)
2035
- `,
2036
- [
2037
- params.universeFingerprint,
2038
- params.whaleRegistryFingerprint,
2039
- params.deleteEventsBeforeMs
2040
- ]
2041
- );
2042
- }
2043
- await client.query("COMMIT");
2044
- return result.rowCount ?? 0;
2045
- } catch (error) {
2046
- await client.query("ROLLBACK");
2047
- throw error;
2048
- } finally {
2049
- client.release();
2050
- }
2051
- }
2052
- async function upsertMarketBreadthRows(rows) {
2053
- if (!rows.length) return;
2054
- await ensureBinanceMarketSchema();
2055
- const pool = getPool();
2056
- const cols = [
2057
- "universe",
2058
- "interval",
2059
- "ts",
2060
- "symbols_count",
2061
- "advancers",
2062
- "decliners",
2063
- "unchanged",
2064
- "advance_decline_ratio",
2065
- "pct_above_ma20",
2066
- "pct_above_ma50",
2067
- "equal_weighted_return",
2068
- "volume_weighted_return",
2069
- "dispersion",
2070
- "btc_return_1h",
2071
- "btc_return_4h",
2072
- "btc_return_24h",
2073
- "alt_basket_return_1h",
2074
- "alt_basket_return_4h",
2075
- "alt_basket_return_24h",
2076
- "btc_vs_alt_return_1h",
2077
- "btc_vs_alt_return_4h",
2078
- "btc_vs_alt_return_24h",
2079
- "btc_turnover_share_1h",
2080
- "btc_turnover_share_24h",
2081
- "btc_turnover_share_change_24h",
2082
- "alt_vol_to_btc_vol_24h",
2083
- "alt_dispersion_24h",
2084
- "btc_alt_regime",
2085
- "source"
2086
- ];
2087
- const maxRows = getSafeBulkInsertRows(cols.length);
2088
- if (rows.length > maxRows) {
2089
- for (let i = 0; i < rows.length; i += maxRows) {
2090
- await upsertMarketBreadthRows(rows.slice(i, i + maxRows));
2091
- }
2092
- return;
2093
- }
2094
- const valuesSql = rows.map(
2095
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2096
- ).join(",");
2097
- const flat = rows.flatMap((row) => [
2098
- row.universe,
2099
- row.interval,
2100
- row.ts,
2101
- row.symbolsCount,
2102
- row.advancers,
2103
- row.decliners,
2104
- row.unchanged,
2105
- row.advanceDeclineRatio ?? null,
2106
- row.pctAboveMa20 ?? null,
2107
- row.pctAboveMa50 ?? null,
2108
- row.equalWeightedReturn ?? null,
2109
- row.volumeWeightedReturn ?? null,
2110
- row.dispersion ?? null,
2111
- row.btcReturn1h ?? null,
2112
- row.btcReturn4h ?? null,
2113
- row.btcReturn24h ?? null,
2114
- row.altBasketReturn1h ?? null,
2115
- row.altBasketReturn4h ?? null,
2116
- row.altBasketReturn24h ?? null,
2117
- row.btcVsAltReturn1h ?? null,
2118
- row.btcVsAltReturn4h ?? null,
2119
- row.btcVsAltReturn24h ?? null,
2120
- row.btcTurnoverShare1h ?? null,
2121
- row.btcTurnoverShare24h ?? null,
2122
- row.btcTurnoverShareChange24h ?? null,
2123
- row.altVolToBtcVol24h ?? null,
2124
- row.altDispersion24h ?? null,
2125
- row.btcAltRegime ?? null,
2126
- row.source ?? null
2127
- ]);
2128
- await pool.query(
2129
- `
2130
- INSERT INTO market_breadth (${cols.join(",")})
2131
- VALUES ${valuesSql}
2132
- ON CONFLICT (universe, interval, ts) DO UPDATE SET
2133
- symbols_count = EXCLUDED.symbols_count,
2134
- advancers = EXCLUDED.advancers,
2135
- decliners = EXCLUDED.decliners,
2136
- unchanged = EXCLUDED.unchanged,
2137
- advance_decline_ratio = COALESCE(EXCLUDED.advance_decline_ratio, market_breadth.advance_decline_ratio),
2138
- pct_above_ma20 = COALESCE(EXCLUDED.pct_above_ma20, market_breadth.pct_above_ma20),
2139
- pct_above_ma50 = COALESCE(EXCLUDED.pct_above_ma50, market_breadth.pct_above_ma50),
2140
- equal_weighted_return = COALESCE(EXCLUDED.equal_weighted_return, market_breadth.equal_weighted_return),
2141
- volume_weighted_return = COALESCE(EXCLUDED.volume_weighted_return, market_breadth.volume_weighted_return),
2142
- dispersion = COALESCE(EXCLUDED.dispersion, market_breadth.dispersion),
2143
- btc_return_1h = COALESCE(EXCLUDED.btc_return_1h, market_breadth.btc_return_1h),
2144
- btc_return_4h = COALESCE(EXCLUDED.btc_return_4h, market_breadth.btc_return_4h),
2145
- btc_return_24h = COALESCE(EXCLUDED.btc_return_24h, market_breadth.btc_return_24h),
2146
- alt_basket_return_1h = COALESCE(EXCLUDED.alt_basket_return_1h, market_breadth.alt_basket_return_1h),
2147
- alt_basket_return_4h = COALESCE(EXCLUDED.alt_basket_return_4h, market_breadth.alt_basket_return_4h),
2148
- alt_basket_return_24h = COALESCE(EXCLUDED.alt_basket_return_24h, market_breadth.alt_basket_return_24h),
2149
- btc_vs_alt_return_1h = COALESCE(EXCLUDED.btc_vs_alt_return_1h, market_breadth.btc_vs_alt_return_1h),
2150
- btc_vs_alt_return_4h = COALESCE(EXCLUDED.btc_vs_alt_return_4h, market_breadth.btc_vs_alt_return_4h),
2151
- btc_vs_alt_return_24h = COALESCE(EXCLUDED.btc_vs_alt_return_24h, market_breadth.btc_vs_alt_return_24h),
2152
- btc_turnover_share_1h = COALESCE(EXCLUDED.btc_turnover_share_1h, market_breadth.btc_turnover_share_1h),
2153
- btc_turnover_share_24h = COALESCE(EXCLUDED.btc_turnover_share_24h, market_breadth.btc_turnover_share_24h),
2154
- btc_turnover_share_change_24h = COALESCE(EXCLUDED.btc_turnover_share_change_24h, market_breadth.btc_turnover_share_change_24h),
2155
- alt_vol_to_btc_vol_24h = COALESCE(EXCLUDED.alt_vol_to_btc_vol_24h, market_breadth.alt_vol_to_btc_vol_24h),
2156
- alt_dispersion_24h = COALESCE(EXCLUDED.alt_dispersion_24h, market_breadth.alt_dispersion_24h),
2157
- btc_alt_regime = COALESCE(EXCLUDED.btc_alt_regime, market_breadth.btc_alt_regime),
2158
- source = COALESCE(EXCLUDED.source, market_breadth.source),
2159
- ingested_at = now()
2160
- `,
2161
- flat
2162
- );
2163
- }
2164
- async function upsertMarketGlobalContextRows(rows) {
2165
- if (!rows.length) return;
2166
- await ensureBinanceMarketSchema();
2167
- const pool = getPool();
2168
- const cols = [
2169
- "source",
2170
- "ts",
2171
- "updated_at_ts",
2172
- "active_cryptocurrencies",
2173
- "active_exchanges",
2174
- "active_market_pairs",
2175
- "markets",
2176
- "total_market_cap_usd",
2177
- "total_volume_usd",
2178
- "total_volume_reported_usd",
2179
- "btc_dominance_pct",
2180
- "eth_dominance_pct",
2181
- "alt_market_cap_usd",
2182
- "alt_volume_usd",
2183
- "alt_volume_reported_usd",
2184
- "btc_to_alt_market_cap_ratio",
2185
- "market_cap_change_pct_24h_usd"
2186
- ];
2187
- const maxRows = getSafeBulkInsertRows(cols.length);
2188
- if (rows.length > maxRows) {
2189
- for (let i = 0; i < rows.length; i += maxRows) {
2190
- await upsertMarketGlobalContextRows(rows.slice(i, i + maxRows));
2191
- }
2192
- return;
2193
- }
2194
- const valuesSql = rows.map(
2195
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2196
- ).join(",");
2197
- const flat = rows.flatMap((row) => [
2198
- row.source,
2199
- row.ts,
2200
- row.updatedAt ?? null,
2201
- row.activeCryptocurrencies ?? null,
2202
- row.activeExchanges ?? null,
2203
- row.activeMarketPairs ?? null,
2204
- row.markets ?? null,
2205
- row.totalMarketCapUsd ?? null,
2206
- row.totalVolumeUsd ?? null,
2207
- row.totalVolumeReportedUsd ?? null,
2208
- row.btcDominancePct ?? null,
2209
- row.ethDominancePct ?? null,
2210
- row.altMarketCapUsd ?? null,
2211
- row.altVolumeUsd ?? null,
2212
- row.altVolumeReportedUsd ?? null,
2213
- row.btcToAltMarketCapRatio ?? null,
2214
- row.marketCapChangePct24hUsd ?? null
2215
- ]);
2216
- await pool.query(
2217
- `
2218
- INSERT INTO market_global_context (${cols.join(",")})
2219
- VALUES ${valuesSql}
2220
- ON CONFLICT (source, ts) DO UPDATE SET
2221
- updated_at_ts = COALESCE(EXCLUDED.updated_at_ts, market_global_context.updated_at_ts),
2222
- active_cryptocurrencies = COALESCE(EXCLUDED.active_cryptocurrencies, market_global_context.active_cryptocurrencies),
2223
- active_exchanges = COALESCE(EXCLUDED.active_exchanges, market_global_context.active_exchanges),
2224
- active_market_pairs = COALESCE(EXCLUDED.active_market_pairs, market_global_context.active_market_pairs),
2225
- markets = COALESCE(EXCLUDED.markets, market_global_context.markets),
2226
- total_market_cap_usd = COALESCE(EXCLUDED.total_market_cap_usd, market_global_context.total_market_cap_usd),
2227
- total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_global_context.total_volume_usd),
2228
- total_volume_reported_usd = COALESCE(EXCLUDED.total_volume_reported_usd, market_global_context.total_volume_reported_usd),
2229
- btc_dominance_pct = COALESCE(EXCLUDED.btc_dominance_pct, market_global_context.btc_dominance_pct),
2230
- eth_dominance_pct = COALESCE(EXCLUDED.eth_dominance_pct, market_global_context.eth_dominance_pct),
2231
- alt_market_cap_usd = COALESCE(EXCLUDED.alt_market_cap_usd, market_global_context.alt_market_cap_usd),
2232
- alt_volume_usd = COALESCE(EXCLUDED.alt_volume_usd, market_global_context.alt_volume_usd),
2233
- alt_volume_reported_usd = COALESCE(EXCLUDED.alt_volume_reported_usd, market_global_context.alt_volume_reported_usd),
2234
- btc_to_alt_market_cap_ratio = COALESCE(EXCLUDED.btc_to_alt_market_cap_ratio, market_global_context.btc_to_alt_market_cap_ratio),
2235
- market_cap_change_pct_24h_usd = COALESCE(EXCLUDED.market_cap_change_pct_24h_usd, market_global_context.market_cap_change_pct_24h_usd),
2236
- ingested_at = now()
2237
- `,
2238
- flat
2239
- );
2240
- }
2241
- async function upsertMarketReferenceAssetContextRows(rows) {
2242
- if (!rows.length) return;
2243
- await ensureBinanceMarketSchema();
2244
- const pool = getPool();
2245
- const cols = [
2246
- "source",
2247
- "symbol",
2248
- "cmc_id",
2249
- "interval",
2250
- "ts",
2251
- "open_usd",
2252
- "high_usd",
2253
- "low_usd",
2254
- "close_usd",
2255
- "volume_usd",
2256
- "market_cap_usd"
2257
- ];
2258
- const maxRows = getSafeBulkInsertRows(cols.length);
2259
- if (rows.length > maxRows) {
2260
- for (let i = 0; i < rows.length; i += maxRows) {
2261
- await upsertMarketReferenceAssetContextRows(rows.slice(i, i + maxRows));
2262
- }
2263
- return;
2264
- }
2265
- const valuesSql = rows.map(
2266
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2267
- ).join(",");
2268
- const flat = rows.flatMap((row) => [
2269
- row.source,
2270
- row.symbol.trim().toUpperCase(),
2271
- Math.trunc(row.cmcId),
2272
- row.interval,
2273
- row.ts,
2274
- row.openUsd ?? null,
2275
- row.highUsd ?? null,
2276
- row.lowUsd ?? null,
2277
- row.closeUsd ?? null,
2278
- row.volumeUsd ?? null,
2279
- row.marketCapUsd ?? null
2280
- ]);
2281
- await pool.query(
2282
- `
2283
- INSERT INTO market_reference_asset_context (${cols.join(",")})
2284
- VALUES ${valuesSql}
2285
- ON CONFLICT (source, symbol, interval, ts) DO UPDATE SET
2286
- cmc_id = EXCLUDED.cmc_id,
2287
- open_usd = COALESCE(EXCLUDED.open_usd, market_reference_asset_context.open_usd),
2288
- high_usd = COALESCE(EXCLUDED.high_usd, market_reference_asset_context.high_usd),
2289
- low_usd = COALESCE(EXCLUDED.low_usd, market_reference_asset_context.low_usd),
2290
- close_usd = COALESCE(EXCLUDED.close_usd, market_reference_asset_context.close_usd),
2291
- volume_usd = COALESCE(EXCLUDED.volume_usd, market_reference_asset_context.volume_usd),
2292
- market_cap_usd = COALESCE(EXCLUDED.market_cap_usd, market_reference_asset_context.market_cap_usd),
2293
- ingested_at = now()
2294
- `,
2295
- flat
2296
- );
2297
- }
2298
- async function upsertMarketCmcExchangeLiquidityContextRows(rows) {
2299
- if (!rows.length) return;
2300
- await ensureBinanceMarketSchema();
2301
- const pool = getPool();
2302
- const cols = [
2303
- "source",
2304
- "interval",
2305
- "ts",
2306
- "exchanges_count",
2307
- "total_volume_usd",
2308
- "binance_volume_usd",
2309
- "binance_volume_share",
2310
- "top_exchange_volume_share",
2311
- "liquidity_regime"
2312
- ];
2313
- const maxRows = getSafeBulkInsertRows(cols.length);
2314
- if (rows.length > maxRows) {
2315
- for (let i = 0; i < rows.length; i += maxRows) {
2316
- await upsertMarketCmcExchangeLiquidityContextRows(
2317
- rows.slice(i, i + maxRows)
2318
- );
2319
- }
2320
- return;
2321
- }
2322
- const valuesSql = rows.map(
2323
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2324
- ).join(",");
2325
- const flat = rows.flatMap((row) => [
2326
- row.source,
2327
- row.interval,
2328
- row.ts,
2329
- Math.trunc(row.exchangesCount),
2330
- row.totalVolumeUsd ?? null,
2331
- row.binanceVolumeUsd ?? null,
2332
- row.binanceVolumeShare ?? null,
2333
- row.topExchangeVolumeShare ?? null,
2334
- row.liquidityRegime ?? null
2335
- ]);
2336
- await pool.query(
2337
- `
2338
- INSERT INTO market_cmc_exchange_liquidity_context (${cols.join(",")})
2339
- VALUES ${valuesSql}
2340
- ON CONFLICT (source, interval, ts) DO UPDATE SET
2341
- exchanges_count = EXCLUDED.exchanges_count,
2342
- total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_cmc_exchange_liquidity_context.total_volume_usd),
2343
- binance_volume_usd = COALESCE(EXCLUDED.binance_volume_usd, market_cmc_exchange_liquidity_context.binance_volume_usd),
2344
- binance_volume_share = COALESCE(EXCLUDED.binance_volume_share, market_cmc_exchange_liquidity_context.binance_volume_share),
2345
- top_exchange_volume_share = COALESCE(EXCLUDED.top_exchange_volume_share, market_cmc_exchange_liquidity_context.top_exchange_volume_share),
2346
- liquidity_regime = COALESCE(EXCLUDED.liquidity_regime, market_cmc_exchange_liquidity_context.liquidity_regime),
2347
- ingested_at = now()
2348
- `,
2349
- flat
2350
- );
2351
- }
2352
- async function upsertMarketCmcFearGreedContextRows(rows) {
2353
- if (!rows.length) return;
2354
- await ensureBinanceMarketSchema();
2355
- const pool = getPool();
2356
- const cols = [
2357
- "source",
2358
- "interval",
2359
- "ts",
2360
- "value",
2361
- "classification",
2362
- "sentiment_regime"
2363
- ];
2364
- const maxRows = getSafeBulkInsertRows(cols.length);
2365
- if (rows.length > maxRows) {
2366
- for (let i = 0; i < rows.length; i += maxRows) {
2367
- await upsertMarketCmcFearGreedContextRows(rows.slice(i, i + maxRows));
2368
- }
2369
- return;
2370
- }
2371
- const valuesSql = rows.map(
2372
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2373
- ).join(",");
2374
- const flat = rows.flatMap((row) => [
2375
- row.source,
2376
- row.interval,
2377
- row.ts,
2378
- Math.trunc(row.value),
2379
- row.classification,
2380
- row.sentimentRegime
2381
- ]);
2382
- await pool.query(
2383
- `
2384
- INSERT INTO market_cmc_fear_greed_context (${cols.join(",")})
2385
- VALUES ${valuesSql}
2386
- ON CONFLICT (source, interval, ts) DO UPDATE SET
2387
- value = EXCLUDED.value,
2388
- classification = EXCLUDED.classification,
2389
- sentiment_regime = EXCLUDED.sentiment_regime,
2390
- ingested_at = now()
2391
- `,
2392
- flat
2393
- );
2394
- }
2395
- async function upsertMarketCmcIndexContextRows(rows) {
2396
- if (!rows.length) return;
2397
- await ensureBinanceMarketSchema();
2398
- const pool = getPool();
2399
- const cols = [
2400
- "source",
2401
- "index_slug",
2402
- "interval",
2403
- "ts",
2404
- "value",
2405
- "constituents_count",
2406
- "top_constituent_symbol",
2407
- "top_constituent_weight_pct",
2408
- "constituents"
2409
- ];
2410
- const maxRows = getSafeBulkInsertRows(cols.length);
2411
- if (rows.length > maxRows) {
2412
- for (let i = 0; i < rows.length; i += maxRows) {
2413
- await upsertMarketCmcIndexContextRows(rows.slice(i, i + maxRows));
2414
- }
2415
- return;
2416
- }
2417
- const valuesSql = rows.map(
2418
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2419
- ).join(",");
2420
- const flat = rows.flatMap((row) => [
2421
- row.source,
2422
- row.indexSlug,
2423
- row.interval,
2424
- row.ts,
2425
- row.value,
2426
- row.constituentsCount ?? null,
2427
- row.topConstituentSymbol ?? null,
2428
- row.topConstituentWeightPct ?? null,
2429
- row.constituents ? JSON.stringify(row.constituents) : null
2430
- ]);
2431
- await pool.query(
2432
- `
2433
- INSERT INTO market_cmc_index_context (${cols.join(",")})
2434
- VALUES ${valuesSql}
2435
- ON CONFLICT (source, index_slug, interval, ts) DO UPDATE SET
2436
- value = EXCLUDED.value,
2437
- constituents_count = COALESCE(EXCLUDED.constituents_count, market_cmc_index_context.constituents_count),
2438
- top_constituent_symbol = COALESCE(EXCLUDED.top_constituent_symbol, market_cmc_index_context.top_constituent_symbol),
2439
- top_constituent_weight_pct = COALESCE(EXCLUDED.top_constituent_weight_pct, market_cmc_index_context.top_constituent_weight_pct),
2440
- constituents = COALESCE(EXCLUDED.constituents, market_cmc_index_context.constituents),
2441
- ingested_at = now()
2442
- `,
2443
- flat
2444
- );
2445
- }
2446
- async function getMarketContextBackfillCoverage(params) {
2447
- const source = String(params.source || "").trim().toLowerCase();
2448
- const scopes = [
2449
- ...new Set(
2450
- params.scopes.map(
2451
- (scope) => String(scope || "").trim().toLowerCase()
2452
- ).filter(Boolean)
2453
- )
2454
- ];
2455
- const interval = String(params.interval || "").trim().toLowerCase();
2456
- if (!source || !scopes.length || !interval) return [];
2457
- await ensureBinanceMarketSchema();
2458
- const pool = getPool();
2459
- const res = await pool.query(
2460
- `
2461
- SELECT
2462
- source,
2463
- scope,
2464
- interval,
2465
- extract(epoch from from_ts)*1000 AS from_ms,
2466
- extract(epoch from to_ts)*1000 AS to_ms,
2467
- rows_count,
2468
- extract(epoch from checked_at)*1000 AS checked_at_ms
2469
- FROM market_context_backfill_coverage
2470
- WHERE source = $1
2471
- AND scope = ANY($2)
2472
- AND interval = $3
2473
- AND from_ts >= to_timestamp($4/1000.0)
2474
- AND to_ts <= to_timestamp($5/1000.0)
2475
- `,
2476
- [source, scopes, interval, params.fromMs, params.toMs]
2477
- );
2478
- return res.rows.map((row) => {
2479
- const checkedAtMs = Number(row.checked_at_ms);
2480
- return {
2481
- source: String(row.source).toLowerCase(),
2482
- scope: String(row.scope).toLowerCase(),
2483
- interval: String(row.interval).toLowerCase(),
2484
- fromMs: Number(row.from_ms),
2485
- toMs: Number(row.to_ms),
2486
- rowsCount: Number(row.rows_count ?? 0),
2487
- ...Number.isFinite(checkedAtMs) ? { checkedAtMs } : {}
2488
- };
2489
- });
2490
- }
2491
- async function upsertMarketContextBackfillCoverage(rows) {
2492
- const normalizedRows = rows.map((row) => ({
2493
- source: String(row.source || "").trim().toLowerCase(),
2494
- scope: String(row.scope || "").trim().toLowerCase(),
2495
- interval: String(row.interval || "").trim().toLowerCase(),
2496
- fromMs: Math.trunc(row.fromMs),
2497
- toMs: Math.trunc(row.toMs),
2498
- rowsCount: Math.max(0, Math.trunc(row.rowsCount))
2499
- })).filter(
2500
- (row) => row.source && row.scope && row.interval && Number.isFinite(row.fromMs) && Number.isFinite(row.toMs) && row.toMs >= row.fromMs
2501
- );
2502
- if (!normalizedRows.length) return;
2503
- await ensureBinanceMarketSchema();
2504
- const pool = getPool();
2505
- const cols = [
2506
- "source",
2507
- "scope",
2508
- "interval",
2509
- "from_ts",
2510
- "to_ts",
2511
- "rows_count"
2512
- ];
2513
- const valuesSql = normalizedRows.map(
2514
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2515
- ).join(",");
2516
- const flat = normalizedRows.flatMap((row) => [
2517
- row.source,
2518
- row.scope,
2519
- row.interval,
2520
- new Date(row.fromMs),
2521
- new Date(row.toMs),
2522
- row.rowsCount
2523
- ]);
2524
- await pool.query(
2525
- `
2526
- INSERT INTO market_context_backfill_coverage (${cols.join(",")})
2527
- VALUES ${valuesSql}
2528
- ON CONFLICT (source, scope, interval, from_ts, to_ts) DO UPDATE SET
2529
- rows_count = EXCLUDED.rows_count,
2530
- checked_at = now()
2531
- `,
2532
- flat
2533
- );
2534
- }
2535
- var toMarketFeatureAge = (rowTs, atMs) => {
2536
- const ageMs = atMs - rowTs.getTime();
2537
- return Number.isFinite(ageMs) ? ageMs : null;
2538
- };
2539
- async function getLatestMarketTradeFlow(params) {
2540
- await prepareMarketContextSchemaForRead("binance");
2541
- const res = await queryMarketContext(
2542
- `
2543
- SELECT
2544
- symbol,
2545
- interval,
2546
- ts,
2547
- trades::int AS trades,
2548
- buy_base_volume AS "buyBaseVolume",
2549
- sell_base_volume AS "sellBaseVolume",
2550
- buy_quote_volume AS "buyQuoteVolume",
2551
- sell_quote_volume AS "sellQuoteVolume",
2552
- net_base_delta AS "netBaseDelta",
2553
- net_quote_delta AS "netQuoteDelta",
2554
- buy_pressure_pct AS "buyPressurePct",
2555
- source
2556
- FROM market_trade_flow
2557
- WHERE symbol = $1
2558
- AND interval = $2
2559
- AND ts <= to_timestamp($3/1000.0)
2560
- ORDER BY ts DESC
2561
- LIMIT 1
2562
- `,
2563
- [params.symbol.toUpperCase(), params.interval, params.atMs],
2564
- params
2565
- );
2566
- const row = res.rows[0];
2567
- if (!row) return null;
2568
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
2569
- return {
2570
- ...row,
2571
- ageMs,
2572
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
2573
- };
2574
- }
2575
- var HYPERLIQUID_CONTEXT_INTERVAL_MS = {
2576
- "1m": 6e4,
2577
- "5m": 5 * 6e4,
2578
- "15m": 15 * 6e4,
2579
- "1h": 60 * 6e4
2580
- };
2581
- async function getHyperliquidWhaleWalletCoverage(params) {
2582
- await ensureHyperliquidWhaleSchema();
2583
- const result = await getPool().query(
2584
- `
2585
- SELECT
2586
- status,
2587
- covered_from_ts,
2588
- covered_to_ts,
2589
- fills_count,
2590
- error,
2591
- checked_at
2592
- FROM hyperliquid_whale_wallet_coverage
2593
- WHERE universe_fingerprint = $1
2594
- AND whale_registry_fingerprint = $2
2595
- AND address = $3
2596
- AND data_model_version = $6
2597
- AND (
2598
- (
2599
- covered_from_ts <= to_timestamp($4/1000.0)
2600
- AND covered_to_ts >= to_timestamp($5/1000.0)
2601
- )
2602
- OR (
2603
- requested_from_ts = to_timestamp($4/1000.0)
2604
- AND requested_to_ts = to_timestamp($5/1000.0)
2605
- )
2606
- )
2607
- ORDER BY checked_at DESC
2608
- LIMIT 1
2609
- `,
2610
- [
2611
- params.universeFingerprint,
2612
- params.whaleRegistryFingerprint,
2613
- params.address.toLowerCase(),
2614
- params.fromMs,
2615
- params.toMs,
2616
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2617
- ]
2618
- );
2619
- const row = result.rows[0];
2620
- if (!row) return null;
2621
- return {
2622
- status: row.covered_from_ts != null && row.covered_to_ts != null && new Date(row.covered_from_ts).getTime() <= params.fromMs && new Date(row.covered_to_ts).getTime() >= params.toMs ? "complete" : String(row.status),
2623
- coveredFromMs: row.covered_from_ts == null ? null : new Date(row.covered_from_ts).getTime(),
2624
- coveredToMs: row.covered_to_ts == null ? null : new Date(row.covered_to_ts).getTime(),
2625
- fillsCount: Number(row.fills_count) || 0,
2626
- error: row.error == null ? null : String(row.error),
2627
- checkedAt: new Date(row.checked_at)
2628
- };
2629
- }
2630
- async function upsertHyperliquidWhaleWalletCoverage(params) {
2631
- await ensureHyperliquidWhaleSchema();
2632
- await getPool().query(
2633
- `
2634
- INSERT INTO hyperliquid_whale_wallet_coverage (
2635
- universe_fingerprint,
2636
- whale_registry_fingerprint,
2637
- address,
2638
- requested_from_ts,
2639
- requested_to_ts,
2640
- covered_from_ts,
2641
- covered_to_ts,
2642
- status,
2643
- fills_count,
2644
- error,
2645
- data_model_version
2646
- ) VALUES (
2647
- $1,
2648
- $2,
2649
- $3,
2650
- to_timestamp($4/1000.0),
2651
- to_timestamp($5/1000.0),
2652
- CASE WHEN $6::double precision IS NULL THEN NULL ELSE to_timestamp($6/1000.0) END,
2653
- CASE WHEN $7::double precision IS NULL THEN NULL ELSE to_timestamp($7/1000.0) END,
2654
- $8,
2655
- $9,
2656
- $10,
2657
- $11
2658
- )
2659
- ON CONFLICT (
2660
- universe_fingerprint,
2661
- whale_registry_fingerprint,
2662
- address,
2663
- requested_from_ts,
2664
- requested_to_ts
2665
- ) DO UPDATE SET
2666
- covered_from_ts = EXCLUDED.covered_from_ts,
2667
- covered_to_ts = EXCLUDED.covered_to_ts,
2668
- status = EXCLUDED.status,
2669
- fills_count = EXCLUDED.fills_count,
2670
- error = EXCLUDED.error,
2671
- data_model_version = EXCLUDED.data_model_version,
2672
- checked_at = now()
2673
- `,
2674
- [
2675
- params.universeFingerprint,
2676
- params.whaleRegistryFingerprint,
2677
- params.address.toLowerCase(),
2678
- params.fromMs,
2679
- params.toMs,
2680
- params.coveredFromMs,
2681
- params.coveredToMs,
2682
- params.status,
2683
- params.fillsCount,
2684
- params.error ?? null,
2685
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2686
- ]
2687
- );
2688
- }
2689
- async function rebuildHyperliquidWhaleCoverageRows(params) {
2690
- await ensureHyperliquidWhaleSchema();
2691
- if (params.toMs <= params.fromMs) return 0;
2692
- const minuteMs = 6e4;
2693
- const defaultChunkMinutes = 7 * 24 * 60;
2694
- const chunkMinutes = Number.isFinite(params.chunkMinutes) && Number(params.chunkMinutes) > 0 ? Math.floor(Number(params.chunkMinutes)) : defaultChunkMinutes;
2695
- const chunkMs = chunkMinutes * minuteMs;
2696
- const totalBuckets = Math.ceil((params.toMs - params.fromMs) / minuteMs);
2697
- const totalChunks = Math.ceil((params.toMs - params.fromMs) / chunkMs);
2698
- let completedBuckets = 0;
2699
- let rows = 0;
2700
- for (let chunkIndex = 0, chunkFromMs = params.fromMs; chunkFromMs < params.toMs; chunkIndex += 1, chunkFromMs += chunkMs) {
2701
- const chunkToMs = Math.min(params.toMs, chunkFromMs + chunkMs);
2702
- const result = await getPool().query(
2703
- `
2704
- WITH normalized_ranges AS (
2705
- SELECT
2706
- address,
2707
- GREATEST(
2708
- to_timestamp($3/1000.0),
2709
- date_trunc('minute', covered_from_ts) +
2710
- CASE
2711
- WHEN covered_from_ts = date_trunc('minute', covered_from_ts)
2712
- THEN interval '0 minutes'
2713
- ELSE interval '1 minute'
2714
- END
2715
- ) AS range_start,
2716
- LEAST(
2717
- to_timestamp($4/1000.0),
2718
- date_trunc('minute', covered_to_ts)
2719
- ) AS range_end
2720
- FROM hyperliquid_whale_wallet_coverage
2721
- WHERE universe_fingerprint = $1
2722
- AND whale_registry_fingerprint = $2
2723
- AND data_model_version = $6
2724
- AND status IN ('complete', 'truncated')
2725
- AND covered_from_ts < to_timestamp($4/1000.0)
2726
- AND covered_to_ts > to_timestamp($3/1000.0)
2727
- ), eligible_ranges AS (
2728
- SELECT *
2729
- FROM normalized_ranges
2730
- WHERE range_start < range_end
2731
- ), ordered_ranges AS (
2732
- SELECT
2733
- *,
2734
- MAX(range_end) OVER (
2735
- PARTITION BY address
2736
- ORDER BY range_start, range_end
2737
- ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
2738
- ) AS previous_max_end
2739
- FROM eligible_ranges
2740
- ), marked_ranges AS (
2741
- SELECT
2742
- *,
2743
- SUM(
2744
- CASE
2745
- WHEN previous_max_end IS NULL OR range_start > previous_max_end
2746
- THEN 1
2747
- ELSE 0
2748
- END
2749
- ) OVER (
2750
- PARTITION BY address
2751
- ORDER BY range_start, range_end
2752
- ) AS range_group
2753
- FROM ordered_ranges
2754
- ), merged_ranges AS (
2755
- SELECT
2756
- address,
2757
- MIN(range_start) AS range_start,
2758
- MAX(range_end) AS range_end
2759
- FROM marked_ranges
2760
- GROUP BY address, range_group
2761
- ), deltas AS (
2762
- SELECT range_start AS ts, 1 AS delta
2763
- FROM merged_ranges
2764
- UNION ALL
2765
- SELECT range_end AS ts, -1 AS delta
2766
- FROM merged_ranges
2767
- ), bucket_deltas AS (
2768
- SELECT ts, SUM(delta)::int AS delta
2769
- FROM deltas
2770
- GROUP BY ts
2771
- ), buckets AS (
2772
- SELECT generate_series(
2773
- to_timestamp($3/1000.0),
2774
- to_timestamp($4/1000.0) - interval '1 minute',
2775
- interval '1 minute'
2776
- ) AS ts
2777
- ), coverage AS (
2778
- SELECT
2779
- buckets.ts,
2780
- SUM(COALESCE(bucket_deltas.delta, 0)) OVER (
2781
- ORDER BY buckets.ts
2782
- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
2783
- )::int AS covered_whales
2784
- FROM buckets
2785
- LEFT JOIN bucket_deltas USING (ts)
2786
- )
2787
- INSERT INTO hyperliquid_whale_coverage_1m (
2788
- ts,
2789
- covered_whales,
2790
- expected_whales,
2791
- coverage_pct,
2792
- universe_fingerprint,
2793
- whale_registry_fingerprint,
2794
- source,
2795
- data_model_version
2796
- )
2797
- SELECT
2798
- ts,
2799
- covered_whales,
2800
- $5,
2801
- CASE WHEN $5 > 0 THEN covered_whales::double precision / $5 ELSE 0 END,
2802
- $1,
2803
- $2,
2804
- 'hyperliquid_user_fills',
2805
- $6
2806
- FROM coverage
2807
- ON CONFLICT (
2808
- universe_fingerprint,
2809
- whale_registry_fingerprint,
2810
- ts
2811
- ) DO UPDATE SET
2812
- covered_whales = EXCLUDED.covered_whales,
2813
- expected_whales = EXCLUDED.expected_whales,
2814
- coverage_pct = EXCLUDED.coverage_pct,
2815
- source = EXCLUDED.source,
2816
- data_model_version = EXCLUDED.data_model_version,
2817
- ingested_at = now()
2818
- `,
2819
- [
2820
- params.universeFingerprint,
2821
- params.whaleRegistryFingerprint,
2822
- chunkFromMs,
2823
- chunkToMs,
2824
- params.expectedWhales,
2825
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2826
- ]
2827
- );
2828
- const chunkBuckets = Math.ceil((chunkToMs - chunkFromMs) / minuteMs);
2829
- completedBuckets = Math.min(totalBuckets, completedBuckets + chunkBuckets);
2830
- rows += result.rowCount ?? 0;
2831
- params.onProgress?.({
2832
- chunkIndex: chunkIndex + 1,
2833
- totalChunks,
2834
- completedBuckets,
2835
- totalBuckets,
2836
- rows
2837
- });
2838
- }
2839
- return rows;
2840
- }
2841
- async function upsertHyperliquidWhaleCoverageRows(rows) {
2842
- if (!rows.length) return;
2843
- await ensureHyperliquidWhaleSchema();
2844
- const cols = [
2845
- "ts",
2846
- "covered_whales",
2847
- "expected_whales",
2848
- "coverage_pct",
2849
- "universe_fingerprint",
2850
- "whale_registry_fingerprint",
2851
- "source",
2852
- "data_model_version"
2853
- ];
2854
- const maxRows = Math.floor(PG_SAFE_MAX_BIND_PARAMS / cols.length);
2855
- if (rows.length > maxRows) {
2856
- for (let index = 0; index < rows.length; index += maxRows) {
2857
- await upsertHyperliquidWhaleCoverageRows(
2858
- rows.slice(index, index + maxRows)
2859
- );
2860
- }
2861
- return;
2862
- }
2863
- const values = [];
2864
- const tuples = rows.map((row, rowIndex) => {
2865
- const offset = rowIndex * cols.length;
2866
- values.push(
2867
- row.ts,
2868
- row.coveredWhales,
2869
- row.expectedWhales,
2870
- row.coveragePct,
2871
- row.universeFingerprint,
2872
- row.whaleRegistryFingerprint,
2873
- row.source ?? null,
2874
- row.dataModelVersion ?? HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2875
- );
2876
- return `(${cols.map((_, colIndex) => `$${offset + colIndex + 1}`).join(",")})`;
2877
- });
2878
- await getPool().query(
2879
- `
2880
- INSERT INTO hyperliquid_whale_coverage_1m (${cols.join(",")})
2881
- VALUES ${tuples.join(",")}
2882
- ON CONFLICT (
2883
- universe_fingerprint,
2884
- whale_registry_fingerprint,
2885
- ts
2886
- ) DO UPDATE SET
2887
- covered_whales = EXCLUDED.covered_whales,
2888
- expected_whales = EXCLUDED.expected_whales,
2889
- coverage_pct = EXCLUDED.coverage_pct,
2890
- source = EXCLUDED.source,
2891
- data_model_version = EXCLUDED.data_model_version,
2892
- ingested_at = now()
2893
- `,
2894
- values
2895
- );
2896
- }
2897
- async function getHyperliquidWhaleCoverageSeriesRows(params) {
2898
- await prepareMarketContextSchemaForRead("hyperliquidWhales");
2899
- const result = await queryMarketContext(
2900
- `
2901
- SELECT
2902
- ts,
2903
- covered_whales,
2904
- expected_whales,
2905
- coverage_pct
2906
- FROM hyperliquid_whale_coverage_1m
2907
- WHERE universe_fingerprint = $1
2908
- AND whale_registry_fingerprint = $2
2909
- AND data_model_version = $3
2910
- AND ts >= to_timestamp($4/1000.0)
2911
- AND ts < to_timestamp($5/1000.0)
2912
- ORDER BY ts
2913
- `,
2914
- [
2915
- params.universeFingerprint,
2916
- params.whaleRegistryFingerprint,
2917
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION,
2918
- params.fromMs,
2919
- params.toMs
2920
- ],
2921
- params
2922
- );
2923
- return result.rows.map((row) => ({
2924
- ts: new Date(row.ts),
2925
- coveredWhales: Number(row.covered_whales) || 0,
2926
- expectedWhales: Number(row.expected_whales) || 0,
2927
- coveragePct: Number(row.coverage_pct) || 0
2928
- }));
2929
- }
2930
- async function getHyperliquidWhaleFlowSeriesRows(params) {
2931
- await prepareMarketContextSchemaForRead("hyperliquidWhales");
2932
- const result = await queryMarketContext(
2933
- `
2934
- SELECT
2935
- ts,
2936
- trades,
2937
- whale_sides,
2938
- whale_addresses,
2939
- buy_notional_usd,
2940
- sell_notional_usd,
2941
- position_aware_whale_sides,
2942
- long_entry_whale_addresses,
2943
- short_entry_whale_addresses,
2944
- long_exit_whale_addresses,
2945
- short_exit_whale_addresses,
2946
- long_entry_notional_usd,
2947
- short_entry_notional_usd,
2948
- long_exit_notional_usd,
2949
- short_exit_notional_usd
2950
- FROM hyperliquid_whale_flow
2951
- WHERE symbol = $1
2952
- AND interval = '1m'
2953
- AND universe_fingerprint = $2
2954
- AND whale_registry_fingerprint = $3
2955
- AND ts >= to_timestamp($4/1000.0)
2956
- AND ts < to_timestamp($5/1000.0)
2957
- ORDER BY ts
2958
- `,
2959
- [
2960
- params.symbol,
2961
- params.universeFingerprint,
2962
- params.whaleRegistryFingerprint,
2963
- params.fromMs,
2964
- params.toMs
2965
- ],
2966
- params
2967
- );
2968
- return result.rows.map((row) => ({
2969
- ts: new Date(row.ts),
2970
- trades: Number(row.trades) || 0,
2971
- whaleSides: Number(row.whale_sides) || 0,
2972
- whaleAddresses: Array.isArray(row.whale_addresses) ? row.whale_addresses.map(String) : [],
2973
- buyNotionalUsd: Number(row.buy_notional_usd) || 0,
2974
- sellNotionalUsd: Number(row.sell_notional_usd) || 0,
2975
- positionAwareWhaleSides: Number(row.position_aware_whale_sides) || 0,
2976
- longEntryWhaleAddresses: Array.isArray(row.long_entry_whale_addresses) ? row.long_entry_whale_addresses.map(String) : [],
2977
- shortEntryWhaleAddresses: Array.isArray(row.short_entry_whale_addresses) ? row.short_entry_whale_addresses.map(String) : [],
2978
- longExitWhaleAddresses: Array.isArray(row.long_exit_whale_addresses) ? row.long_exit_whale_addresses.map(String) : [],
2979
- shortExitWhaleAddresses: Array.isArray(row.short_exit_whale_addresses) ? row.short_exit_whale_addresses.map(String) : [],
2980
- longEntryNotionalUsd: Number(row.long_entry_notional_usd) || 0,
2981
- shortEntryNotionalUsd: Number(row.short_entry_notional_usd) || 0,
2982
- longExitNotionalUsd: Number(row.long_exit_notional_usd) || 0,
2983
- shortExitNotionalUsd: Number(row.short_exit_notional_usd) || 0
2984
- }));
2985
- }
2986
- async function getHyperliquidWhaleFlowAggregate(params) {
2987
- await prepareMarketContextSchemaForRead("hyperliquidWhales");
2988
- const intervalMs = HYPERLIQUID_CONTEXT_INTERVAL_MS[params.interval];
2989
- const expectedBuckets = Math.ceil(intervalMs / 6e4);
2990
- const res = await queryMarketContext(
2991
- `
2992
- WITH coverage_rows AS (
2993
- SELECT *
2994
- FROM hyperliquid_whale_coverage_1m
2995
- WHERE universe_fingerprint = $2
2996
- AND whale_registry_fingerprint = $3
2997
- AND data_model_version = $6
2998
- AND ts >= to_timestamp(
2999
- ($4::double precision - $5::double precision) / 1000.0
3000
- )
3001
- AND ts < to_timestamp($4/1000.0)
3002
- ), coverage_summary AS (
3003
- SELECT
3004
- COUNT(*)::int AS coverage_buckets,
3005
- MAX(ts) AS coverage_as_of_ts,
3006
- MIN(covered_whales)::int AS covered_whales,
3007
- MAX(expected_whales)::int AS expected_whales,
3008
- MIN(coverage_pct) AS coverage_pct
3009
- FROM coverage_rows
3010
- ), window_rows AS (
3011
- SELECT *
3012
- FROM hyperliquid_whale_flow
3013
- WHERE symbol = $1
3014
- AND interval = '1m'
3015
- AND universe_fingerprint = $2
3016
- AND whale_registry_fingerprint = $3
3017
- AND ts >= to_timestamp(
3018
- ($4::double precision - $5::double precision) / 1000.0
3019
- )
3020
- AND ts < to_timestamp($4/1000.0)
3021
- ), unique_addresses AS (
3022
- SELECT COUNT(DISTINCT address)::int AS unique_whales
3023
- FROM window_rows
3024
- CROSS JOIN LATERAL UNNEST(whale_addresses) AS expanded(address)
3025
- ), directional_counts AS (
3026
- SELECT
3027
- (
3028
- SELECT COUNT(DISTINCT address)::int
3029
- FROM window_rows
3030
- CROSS JOIN LATERAL UNNEST(long_entry_whale_addresses) AS expanded(address)
3031
- ) AS long_entry_whales,
3032
- (
3033
- SELECT COUNT(DISTINCT address)::int
3034
- FROM window_rows
3035
- CROSS JOIN LATERAL UNNEST(short_entry_whale_addresses) AS expanded(address)
3036
- ) AS short_entry_whales,
3037
- (
3038
- SELECT COUNT(DISTINCT address)::int
3039
- FROM window_rows
3040
- CROSS JOIN LATERAL UNNEST(long_exit_whale_addresses) AS expanded(address)
3041
- ) AS long_exit_whales,
3042
- (
3043
- SELECT COUNT(DISTINCT address)::int
3044
- FROM window_rows
3045
- CROSS JOIN LATERAL UNNEST(short_exit_whale_addresses) AS expanded(address)
3046
- ) AS short_exit_whales
3047
- )
3048
- SELECT
3049
- $1::text AS symbol,
3050
- coverage_summary.coverage_as_of_ts AS as_of_ts,
3051
- coverage_summary.coverage_buckets,
3052
- coverage_summary.covered_whales,
3053
- coverage_summary.expected_whales,
3054
- coverage_summary.coverage_pct,
3055
- COALESCE((SELECT SUM(trades) FROM window_rows), 0)::int AS trades,
3056
- COALESCE((SELECT SUM(whale_sides) FROM window_rows), 0)::int AS whale_sides,
3057
- COALESCE((SELECT unique_whales FROM unique_addresses), 0)::int AS unique_whales,
3058
- COALESCE((SELECT SUM(buy_notional_usd) FROM window_rows), 0) AS buy_notional_usd,
3059
- COALESCE((SELECT SUM(sell_notional_usd) FROM window_rows), 0) AS sell_notional_usd,
3060
- COALESCE((SELECT SUM(net_notional_usd) FROM window_rows), 0) AS net_notional_usd,
3061
- CASE
3062
- WHEN COALESCE((SELECT SUM(buy_notional_usd + sell_notional_usd) FROM window_rows), 0) > 0
3063
- THEN (SELECT SUM(buy_notional_usd) FROM window_rows) /
3064
- (SELECT SUM(buy_notional_usd + sell_notional_usd) FROM window_rows)
3065
- ELSE NULL
3066
- END AS buy_share_pct,
3067
- COALESCE((SELECT SUM(position_aware_whale_sides) FROM window_rows), 0)::int
3068
- AS position_aware_whale_sides,
3069
- CASE
3070
- WHEN COALESCE((SELECT SUM(whale_sides) FROM window_rows), 0) > 0
3071
- THEN COALESCE((SELECT SUM(position_aware_whale_sides) FROM window_rows), 0)::double precision /
3072
- (SELECT SUM(whale_sides) FROM window_rows)
3073
- ELSE 0
3074
- END AS position_aware_pct,
3075
- COALESCE((SELECT long_entry_whales FROM directional_counts), 0)::int AS long_entry_whales,
3076
- COALESCE((SELECT short_entry_whales FROM directional_counts), 0)::int AS short_entry_whales,
3077
- COALESCE((SELECT long_exit_whales FROM directional_counts), 0)::int AS long_exit_whales,
3078
- COALESCE((SELECT short_exit_whales FROM directional_counts), 0)::int AS short_exit_whales,
3079
- COALESCE((SELECT SUM(long_entry_notional_usd) FROM window_rows), 0)
3080
- AS long_entry_notional_usd,
3081
- COALESCE((SELECT SUM(short_entry_notional_usd) FROM window_rows), 0)
3082
- AS short_entry_notional_usd,
3083
- COALESCE((SELECT SUM(long_exit_notional_usd) FROM window_rows), 0)
3084
- AS long_exit_notional_usd,
3085
- COALESCE((SELECT SUM(short_exit_notional_usd) FROM window_rows), 0)
3086
- AS short_exit_notional_usd,
3087
- COALESCE((SELECT SUM(entry_net_notional_usd) FROM window_rows), 0)
3088
- AS entry_net_notional_usd,
3089
- CASE
3090
- WHEN COALESCE((SELECT SUM(long_entry_notional_usd + short_entry_notional_usd) FROM window_rows), 0) > 0
3091
- THEN (SELECT SUM(long_entry_notional_usd) FROM window_rows) /
3092
- (SELECT SUM(long_entry_notional_usd + short_entry_notional_usd) FROM window_rows)
3093
- ELSE NULL
3094
- END AS entry_long_share_pct,
3095
- (SELECT MAX(source) FROM window_rows) AS source
3096
- FROM coverage_summary
3097
- `,
3098
- [
3099
- params.symbol,
3100
- params.universeFingerprint,
3101
- params.whaleRegistryFingerprint,
3102
- params.decisionTimeMs,
3103
- intervalMs,
3104
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3105
- ],
3106
- params
3107
- );
3108
- const row = res.rows[0];
3109
- if (!row?.as_of_ts || Number(row.coverage_buckets) !== expectedBuckets || Number(row.covered_whales) <= 0) {
3110
- return null;
3111
- }
3112
- const asOfTs = new Date(row.as_of_ts);
3113
- const ageMs = params.decisionTimeMs - (asOfTs.getTime() + 6e4);
3114
- return {
3115
- symbol: params.symbol,
3116
- interval: params.interval,
3117
- asOfTs,
3118
- windowEndTs: new Date(params.decisionTimeMs),
3119
- trades: Number(row.trades) || 0,
3120
- whaleSides: Number(row.whale_sides) || 0,
3121
- uniqueWhales: Number(row.unique_whales) || 0,
3122
- coveredWhales: Number(row.covered_whales) || 0,
3123
- expectedWhales: Number(row.expected_whales) || 0,
3124
- coveragePct: Number(row.coverage_pct) || 0,
3125
- buyNotionalUsd: Number(row.buy_notional_usd) || 0,
3126
- sellNotionalUsd: Number(row.sell_notional_usd) || 0,
3127
- netNotionalUsd: Number(row.net_notional_usd) || 0,
3128
- buySharePct: row.buy_share_pct == null ? null : Number(row.buy_share_pct) || 0,
3129
- positionAwareWhaleSides: Number(row.position_aware_whale_sides) || 0,
3130
- positionAwarePct: Number(row.position_aware_pct) || 0,
3131
- longEntryWhales: Number(row.long_entry_whales) || 0,
3132
- shortEntryWhales: Number(row.short_entry_whales) || 0,
3133
- longExitWhales: Number(row.long_exit_whales) || 0,
3134
- shortExitWhales: Number(row.short_exit_whales) || 0,
3135
- longEntryNotionalUsd: Number(row.long_entry_notional_usd) || 0,
3136
- shortEntryNotionalUsd: Number(row.short_entry_notional_usd) || 0,
3137
- longExitNotionalUsd: Number(row.long_exit_notional_usd) || 0,
3138
- shortExitNotionalUsd: Number(row.short_exit_notional_usd) || 0,
3139
- entryNetNotionalUsd: Number(row.entry_net_notional_usd) || 0,
3140
- entryLongSharePct: row.entry_long_share_pct == null ? null : Number(row.entry_long_share_pct) || 0,
3141
- universeFingerprint: params.universeFingerprint,
3142
- whaleRegistryFingerprint: params.whaleRegistryFingerprint,
3143
- source: row.source == null ? null : String(row.source),
3144
- ageMs,
3145
- stale: ageMs < 0 || (params.maxAgeMs != null && Number.isFinite(params.maxAgeMs) ? ageMs > params.maxAgeMs : false)
3146
- };
3147
- }
3148
- async function hasHyperliquidWhaleBackfillCoverage(params) {
3149
- await ensureHyperliquidWhaleSchema();
3150
- const expectedBuckets = Math.max(
3151
- 0,
3152
- Math.ceil((params.toMs - params.fromMs) / 6e4)
3153
- );
3154
- const result = await getPool().query(
3155
- `
3156
- SELECT
3157
- COUNT(*)::int AS buckets,
3158
- COUNT(*) FILTER (
3159
- WHERE covered_whales = expected_whales
3160
- )::int AS complete_buckets
3161
- FROM hyperliquid_whale_coverage_1m
3162
- WHERE universe_fingerprint = $1
3163
- AND whale_registry_fingerprint = $2
3164
- AND data_model_version = $5
3165
- AND ts >= to_timestamp($3/1000.0)
3166
- AND ts < to_timestamp($4/1000.0)
3167
- `,
3168
- [
3169
- params.universeFingerprint,
3170
- params.whaleRegistryFingerprint,
3171
- params.fromMs,
3172
- params.toMs,
3173
- HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3174
- ]
3175
- );
3176
- return Number(result.rows[0]?.buckets) === expectedBuckets && Number(result.rows[0]?.complete_buckets) === expectedBuckets;
3177
- }
3178
- async function getLatestMarketBreadth(params) {
3179
- await prepareMarketContextSchemaForRead("binance");
3180
- const res = await queryMarketContext(
3181
- `
3182
- SELECT
3183
- universe,
3184
- interval,
3185
- ts,
3186
- symbols_count::int AS "symbolsCount",
3187
- advancers::int AS advancers,
3188
- decliners::int AS decliners,
3189
- unchanged::int AS unchanged,
3190
- advance_decline_ratio AS "advanceDeclineRatio",
3191
- pct_above_ma20 AS "pctAboveMa20",
3192
- pct_above_ma50 AS "pctAboveMa50",
3193
- equal_weighted_return AS "equalWeightedReturn",
3194
- volume_weighted_return AS "volumeWeightedReturn",
3195
- dispersion,
3196
- btc_return_1h AS "btcReturn1h",
3197
- btc_return_4h AS "btcReturn4h",
3198
- btc_return_24h AS "btcReturn24h",
3199
- alt_basket_return_1h AS "altBasketReturn1h",
3200
- alt_basket_return_4h AS "altBasketReturn4h",
3201
- alt_basket_return_24h AS "altBasketReturn24h",
3202
- btc_vs_alt_return_1h AS "btcVsAltReturn1h",
3203
- btc_vs_alt_return_4h AS "btcVsAltReturn4h",
3204
- btc_vs_alt_return_24h AS "btcVsAltReturn24h",
3205
- btc_turnover_share_1h AS "btcTurnoverShare1h",
3206
- btc_turnover_share_24h AS "btcTurnoverShare24h",
3207
- btc_turnover_share_change_24h AS "btcTurnoverShareChange24h",
3208
- alt_vol_to_btc_vol_24h AS "altVolToBtcVol24h",
3209
- alt_dispersion_24h AS "altDispersion24h",
3210
- btc_alt_regime AS "btcAltRegime",
3211
- source
3212
- FROM market_breadth
3213
- WHERE universe = $1
3214
- AND interval = $2
3215
- AND ts <= to_timestamp($3/1000.0)
3216
- ORDER BY ts DESC
3217
- LIMIT 1
3218
- `,
3219
- [params.universe, params.interval, params.atMs],
3220
- params
3221
- );
3222
- const row = res.rows[0];
3223
- if (!row) return null;
3224
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3225
- return {
3226
- ...row,
3227
- ageMs,
3228
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
3229
- };
3230
- }
3231
- async function getLatestMarketGlobalContext(params) {
3232
- await prepareMarketContextSchemaForRead("coinmarketcap");
3233
- const source = params.source ?? "coinmarketcap_global";
3234
- const res = await queryMarketContext(
3235
- `
3236
- SELECT
3237
- source,
3238
- ts,
3239
- updated_at_ts AS "updatedAt",
3240
- active_cryptocurrencies::int AS "activeCryptocurrencies",
3241
- active_exchanges::int AS "activeExchanges",
3242
- active_market_pairs::int AS "activeMarketPairs",
3243
- markets::int AS markets,
3244
- total_market_cap_usd AS "totalMarketCapUsd",
3245
- total_volume_usd AS "totalVolumeUsd",
3246
- total_volume_reported_usd AS "totalVolumeReportedUsd",
3247
- btc_dominance_pct AS "btcDominancePct",
3248
- eth_dominance_pct AS "ethDominancePct",
3249
- alt_market_cap_usd AS "altMarketCapUsd",
3250
- alt_volume_usd AS "altVolumeUsd",
3251
- alt_volume_reported_usd AS "altVolumeReportedUsd",
3252
- btc_to_alt_market_cap_ratio AS "btcToAltMarketCapRatio",
3253
- market_cap_change_pct_24h_usd AS "marketCapChangePct24hUsd"
3254
- FROM market_global_context
3255
- WHERE source = $1
3256
- AND ts + CASE
3257
- WHEN source = 'coinmarketcap_global' THEN interval '1 day'
3258
- ELSE interval '0 seconds'
3259
- END <= to_timestamp($2/1000.0)
3260
- ORDER BY ts DESC
3261
- LIMIT 1
3262
- `,
3263
- [source, params.atMs],
3264
- params
3265
- );
3266
- const row = res.rows[0];
3267
- if (!row) return null;
3268
- const previousRes = await queryMarketContext(
3269
- `
3270
- SELECT
3271
- btc_dominance_pct AS "btcDominancePct",
3272
- eth_dominance_pct AS "ethDominancePct",
3273
- alt_market_cap_usd AS "altMarketCapUsd",
3274
- alt_volume_usd AS "altVolumeUsd"
3275
- FROM market_global_context
3276
- WHERE source = $1
3277
- AND ts <= $2::timestamptz - interval '24 hours'
3278
- ORDER BY ts DESC
3279
- LIMIT 1
3280
- `,
3281
- [source, row.ts],
3282
- params
3283
- );
3284
- const previousDominance = previousRes.rows[0]?.btcDominancePct == null ? null : Number(previousRes.rows[0].btcDominancePct);
3285
- const previousEthDominance = previousRes.rows[0]?.ethDominancePct == null ? null : Number(previousRes.rows[0].ethDominancePct);
3286
- const previousAltMarketCap = previousRes.rows[0]?.altMarketCapUsd == null ? null : Number(previousRes.rows[0].altMarketCapUsd);
3287
- const previousAltVolume = previousRes.rows[0]?.altVolumeUsd == null ? null : Number(previousRes.rows[0].altVolumeUsd);
3288
- const currentDominance = row.btcDominancePct == null ? null : Number(row.btcDominancePct);
3289
- const currentEthDominance = row.ethDominancePct == null ? null : Number(row.ethDominancePct);
3290
- const currentAltMarketCap = row.altMarketCapUsd == null ? null : Number(row.altMarketCapUsd);
3291
- const currentAltVolume = row.altVolumeUsd == null ? null : Number(row.altVolumeUsd);
3292
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3293
- return {
3294
- ...row,
3295
- ageMs,
3296
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3297
- btcDominanceChange24hPct: currentDominance != null && previousDominance != null ? currentDominance - previousDominance : null,
3298
- ethDominanceChange24hPct: currentEthDominance != null && previousEthDominance != null ? currentEthDominance - previousEthDominance : null,
3299
- altMarketCapChange24hPct: currentAltMarketCap != null && previousAltMarketCap != null && previousAltMarketCap > 0 ? (currentAltMarketCap - previousAltMarketCap) / previousAltMarketCap : null,
3300
- altVolumeChange24hPct: currentAltVolume != null && previousAltVolume != null && previousAltVolume > 0 ? (currentAltVolume - previousAltVolume) / previousAltVolume : null
3301
- };
3302
- }
3303
- async function getMarketGlobalContextCoverage(params) {
3304
- await ensureBinanceMarketSchema();
3305
- const pool = getPool();
3306
- const res = await pool.query(
3307
- `
3308
- SELECT
3309
- extract(epoch from MIN(ts))*1000 AS first_ms,
3310
- extract(epoch from MAX(ts))*1000 AS last_ms,
3311
- COUNT(*)::int AS rows
3312
- FROM market_global_context
3313
- WHERE source = $1
3314
- AND ts >= to_timestamp($2/1000.0)
3315
- AND ts <= to_timestamp($3/1000.0)
3316
- `,
3317
- [params.source, params.startMs, params.endMs]
3318
- );
3319
- const row = res.rows[0];
3320
- const rows = Number(row?.rows ?? 0);
3321
- const firstMs = Number(row?.first_ms);
3322
- const lastMs = Number(row?.last_ms);
3323
- if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
3324
- return null;
3325
- }
3326
- return { firstMs, lastMs, rows };
3327
- }
3328
- async function getMarketReferenceAssetContextCoverage(params) {
3329
- const symbols = [
3330
- ...new Set(
3331
- params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
3332
- )
3333
- ];
3334
- const coverage = /* @__PURE__ */ new Map();
3335
- if (!symbols.length) return coverage;
3336
- await ensureBinanceMarketSchema();
3337
- const pool = getPool();
3338
- const res = await pool.query(
3339
- `
3340
- SELECT
3341
- symbol,
3342
- extract(epoch from MIN(ts))*1000 AS first_ms,
3343
- extract(epoch from MAX(ts))*1000 AS last_ms,
3344
- COUNT(*)::int AS rows
3345
- FROM market_reference_asset_context
3346
- WHERE source = $1
3347
- AND symbol = ANY($2)
3348
- AND interval = $3
3349
- AND ts >= to_timestamp($4/1000.0)
3350
- AND ts <= to_timestamp($5/1000.0)
3351
- GROUP BY symbol
3352
- `,
3353
- [params.source, symbols, params.interval, params.startMs, params.endMs]
3354
- );
3355
- for (const row of res.rows) {
3356
- const firstMs = Number(row.first_ms);
3357
- const lastMs = Number(row.last_ms);
3358
- const rows = Number(row.rows);
3359
- if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
3360
- coverage.set(row.symbol.toUpperCase(), { firstMs, lastMs, rows });
3361
- }
3362
- }
3363
- return coverage;
3364
- }
3365
- async function getLatestMarketReferenceAssetContexts(params) {
3366
- const source = params.source ?? "coinmarketcap_reference_asset";
3367
- const interval = params.interval ?? "1d";
3368
- const symbols = [
3369
- ...new Set(
3370
- params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
3371
- )
3372
- ];
3373
- const rows = /* @__PURE__ */ new Map();
3374
- if (!symbols.length) return rows;
3375
- await prepareMarketContextSchemaForRead("coinmarketcap");
3376
- const res = await queryMarketContext(
3377
- `
3378
- SELECT DISTINCT ON (symbol)
3379
- source,
3380
- symbol,
3381
- cmc_id AS "cmcId",
3382
- interval,
3383
- ts,
3384
- open_usd AS "openUsd",
3385
- high_usd AS "highUsd",
3386
- low_usd AS "lowUsd",
3387
- close_usd AS "closeUsd",
3388
- volume_usd AS "volumeUsd",
3389
- market_cap_usd AS "marketCapUsd"
3390
- FROM market_reference_asset_context
3391
- WHERE source = $1
3392
- AND symbol = ANY($2)
3393
- AND interval = $3
3394
- AND ts + CASE interval
3395
- WHEN '1d' THEN interval '1 day'
3396
- WHEN '1h' THEN interval '1 hour'
3397
- ELSE interval '0 seconds'
3398
- END <= to_timestamp($4/1000.0)
3399
- ORDER BY symbol ASC, ts DESC
3400
- `,
3401
- [source, symbols, interval, params.atMs],
3402
- params
3403
- );
3404
- for (const row of res.rows) {
3405
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3406
- rows.set(row.symbol.toUpperCase(), {
3407
- ...row,
3408
- ageMs,
3409
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
3410
- });
3411
- }
3412
- return rows;
3413
- }
3414
- async function getLatestMarketCmcExchangeLiquidityContext(params) {
3415
- await prepareMarketContextSchemaForRead("coinmarketcap");
3416
- const source = params.source ?? "coinmarketcap_exchange_liquidity";
3417
- const interval = params.interval ?? "1d";
3418
- const res = await queryMarketContext(
3419
- `
3420
- SELECT
3421
- source,
3422
- interval,
3423
- ts,
3424
- exchanges_count::int AS "exchangesCount",
3425
- total_volume_usd AS "totalVolumeUsd",
3426
- binance_volume_usd AS "binanceVolumeUsd",
3427
- binance_volume_share AS "binanceVolumeShare",
3428
- top_exchange_volume_share AS "topExchangeVolumeShare",
3429
- liquidity_regime AS "liquidityRegime"
3430
- FROM market_cmc_exchange_liquidity_context
3431
- WHERE source = $1
3432
- AND interval = $2
3433
- AND ts + CASE interval
3434
- WHEN '1d' THEN interval '1 day'
3435
- WHEN '1h' THEN interval '1 hour'
3436
- ELSE interval '0 seconds'
3437
- END <= to_timestamp($3/1000.0)
3438
- ORDER BY ts DESC
3439
- LIMIT 1
3440
- `,
3441
- [source, interval, params.atMs],
3442
- params
3443
- );
3444
- const row = res.rows[0];
3445
- if (!row) return null;
3446
- const previousRes = await queryMarketContext(
3447
- `
3448
- SELECT total_volume_usd AS "totalVolumeUsd"
3449
- FROM market_cmc_exchange_liquidity_context
3450
- WHERE source = $1
3451
- AND interval = $2
3452
- AND ts <= $3::timestamptz - interval '24 hours'
3453
- ORDER BY ts DESC
3454
- LIMIT 1
3455
- `,
3456
- [source, interval, row.ts],
3457
- params
3458
- );
3459
- const currentTotal = row.totalVolumeUsd == null ? null : Number(row.totalVolumeUsd);
3460
- const previousTotal = previousRes.rows[0]?.totalVolumeUsd == null ? null : Number(previousRes.rows[0].totalVolumeUsd);
3461
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3462
- return {
3463
- ...row,
3464
- ageMs,
3465
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3466
- totalVolumeChange24hPct: currentTotal != null && previousTotal != null && previousTotal > 0 ? (currentTotal - previousTotal) / previousTotal : null
3467
- };
3468
- }
3469
- async function getLatestMarketCmcIndexContexts(params) {
3470
- const source = params.source ?? "coinmarketcap_index";
3471
- const interval = params.interval ?? "1d";
3472
- const indexSlugs = [
3473
- ...new Set(
3474
- params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
3475
- (slug) => ["cmc100", "cmc20"].includes(slug)
3476
- )
3477
- )
3478
- ];
3479
- const rows = /* @__PURE__ */ new Map();
3480
- if (!indexSlugs.length) return rows;
3481
- await prepareMarketContextSchemaForRead("coinmarketcap");
3482
- const res = await queryMarketContext(
3483
- `
3484
- SELECT DISTINCT ON (index_slug)
3485
- source,
3486
- index_slug AS "indexSlug",
3487
- interval,
3488
- ts,
3489
- value,
3490
- constituents_count::int AS "constituentsCount",
3491
- top_constituent_symbol AS "topConstituentSymbol",
3492
- top_constituent_weight_pct AS "topConstituentWeightPct",
3493
- constituents
3494
- FROM market_cmc_index_context
3495
- WHERE source = $1
3496
- AND index_slug = ANY($2)
3497
- AND interval = $3
3498
- AND ts + CASE interval
3499
- WHEN '1d' THEN interval '1 day'
3500
- WHEN '1h' THEN interval '1 hour'
3501
- ELSE interval '0 seconds'
3502
- END <= to_timestamp($4/1000.0)
3503
- ORDER BY index_slug ASC, ts DESC
3504
- `,
3505
- [source, indexSlugs, interval, params.atMs],
3506
- params
3507
- );
3508
- for (const row of res.rows) {
3509
- const previousRes = await queryMarketContext(
3510
- `
3511
- SELECT value
3512
- FROM market_cmc_index_context
3513
- WHERE source = $1
3514
- AND index_slug = $2
3515
- AND interval = $3
3516
- AND ts <= $4::timestamptz - interval '24 hours'
3517
- ORDER BY ts DESC
3518
- LIMIT 1
3519
- `,
3520
- [source, row.indexSlug, interval, row.ts],
3521
- params
3522
- );
3523
- const currentValue = row.value == null ? null : Number(row.value);
3524
- const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
3525
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3526
- rows.set(row.indexSlug, {
3527
- ...row,
3528
- ageMs,
3529
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3530
- valueChange24hPct: currentValue != null && previousValue != null && previousValue > 0 ? (currentValue - previousValue) / previousValue : null
3531
- });
3532
- }
3533
- return rows;
3534
- }
3535
- async function getLatestMarketCmcFearGreedContext(params) {
3536
- await prepareMarketContextSchemaForRead("coinmarketcap");
3537
- const source = params.source ?? "coinmarketcap_fear_greed";
3538
- const interval = params.interval ?? "1d";
3539
- const res = await queryMarketContext(
3540
- `
3541
- SELECT
3542
- source,
3543
- interval,
3544
- ts,
3545
- value::int AS value,
3546
- classification,
3547
- sentiment_regime AS "sentimentRegime"
3548
- FROM market_cmc_fear_greed_context
3549
- WHERE source = $1
3550
- AND interval = $2
3551
- AND ts + CASE interval
3552
- WHEN '1d' THEN interval '1 day'
3553
- WHEN '1h' THEN interval '1 hour'
3554
- ELSE interval '0 seconds'
3555
- END <= to_timestamp($3/1000.0)
3556
- ORDER BY ts DESC
3557
- LIMIT 1
3558
- `,
3559
- [source, interval, params.atMs],
3560
- params
3561
- );
3562
- const row = res.rows[0];
3563
- if (!row) return null;
3564
- const previousRes = await queryMarketContext(
3565
- `
3566
- SELECT
3567
- value::int AS value,
3568
- '24h' AS bucket
3569
- FROM market_cmc_fear_greed_context
3570
- WHERE source = $1
3571
- AND interval = $2
3572
- AND ts <= $3::timestamptz - interval '24 hours'
3573
- ORDER BY ts DESC
3574
- LIMIT 1
3575
- `,
3576
- [source, interval, row.ts],
3577
- params
3578
- );
3579
- const previous7dRes = await queryMarketContext(
3580
- `
3581
- SELECT value::int AS value
3582
- FROM market_cmc_fear_greed_context
3583
- WHERE source = $1
3584
- AND interval = $2
3585
- AND ts <= $3::timestamptz - interval '7 days'
3586
- ORDER BY ts DESC
3587
- LIMIT 1
3588
- `,
3589
- [source, interval, row.ts],
3590
- params
3591
- );
3592
- const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
3593
- const previous7dValue = previous7dRes.rows[0]?.value == null ? null : Number(previous7dRes.rows[0].value);
3594
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3595
- return {
3596
- ...row,
3597
- ageMs,
3598
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3599
- valueChange24h: previousValue == null ? null : row.value - previousValue,
3600
- valueChange7d: previous7dValue == null ? null : row.value - previous7dValue
3601
- };
3602
- }
3603
- async function getMarketCmcFearGreedContextCoverage(params) {
3604
- await ensureBinanceMarketSchema();
3605
- const pool = getPool();
3606
- const res = await pool.query(
3607
- `
3608
- SELECT
3609
- extract(epoch from MIN(ts))*1000 AS first_ms,
3610
- extract(epoch from MAX(ts))*1000 AS last_ms,
3611
- COUNT(*)::int AS rows
3612
- FROM market_cmc_fear_greed_context
3613
- WHERE source = $1
3614
- AND interval = $2
3615
- AND ts >= to_timestamp($3/1000.0)
3616
- AND ts <= to_timestamp($4/1000.0)
3617
- `,
3618
- [params.source, params.interval, params.startMs, params.endMs]
3619
- );
3620
- const rows = Number(res.rows[0]?.rows ?? 0);
3621
- const firstMs = Number(res.rows[0]?.first_ms);
3622
- const lastMs = Number(res.rows[0]?.last_ms);
3623
- if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
3624
- return null;
3625
- }
3626
- return { firstMs, lastMs, rows };
3627
- }
3628
- async function getMarketCmcExchangeLiquidityContextCoverage(params) {
3629
- await ensureBinanceMarketSchema();
3630
- const pool = getPool();
3631
- const res = await pool.query(
3632
- `
3633
- SELECT
3634
- extract(epoch from MIN(ts))*1000 AS first_ms,
3635
- extract(epoch from MAX(ts))*1000 AS last_ms,
3636
- COUNT(*)::int AS rows
3637
- FROM market_cmc_exchange_liquidity_context
3638
- WHERE source = $1
3639
- AND interval = $2
3640
- AND ts >= to_timestamp($3/1000.0)
3641
- AND ts <= to_timestamp($4/1000.0)
3642
- `,
3643
- [params.source, params.interval, params.startMs, params.endMs]
3644
- );
3645
- const rows = Number(res.rows[0]?.rows ?? 0);
3646
- const firstMs = Number(res.rows[0]?.first_ms);
3647
- const lastMs = Number(res.rows[0]?.last_ms);
3648
- if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
3649
- return null;
3650
- }
3651
- return { firstMs, lastMs, rows };
3652
- }
3653
- async function getMarketCmcIndexContextCoverage(params) {
3654
- const indexSlugs = [
3655
- ...new Set(
3656
- params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
3657
- (slug) => ["cmc100", "cmc20"].includes(slug)
3658
- )
3659
- )
3660
- ];
3661
- const coverage = /* @__PURE__ */ new Map();
3662
- if (!indexSlugs.length) return coverage;
3663
- await ensureBinanceMarketSchema();
3664
- const pool = getPool();
3665
- const res = await pool.query(
3666
- `
3667
- SELECT
3668
- index_slug,
3669
- extract(epoch from MIN(ts))*1000 AS first_ms,
3670
- extract(epoch from MAX(ts))*1000 AS last_ms,
3671
- COUNT(*)::int AS rows
3672
- FROM market_cmc_index_context
3673
- WHERE source = $1
3674
- AND index_slug = ANY($2)
3675
- AND interval = $3
3676
- AND ts >= to_timestamp($4/1000.0)
3677
- AND ts <= to_timestamp($5/1000.0)
3678
- GROUP BY index_slug
3679
- `,
3680
- [params.source, indexSlugs, params.interval, params.startMs, params.endMs]
3681
- );
3682
- for (const row of res.rows) {
3683
- const indexSlug = row.index_slug;
3684
- const firstMs = Number(row.first_ms);
3685
- const lastMs = Number(row.last_ms);
3686
- const rows = Number(row.rows);
3687
- if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
3688
- coverage.set(indexSlug, { firstMs, lastMs, rows });
3689
- }
3690
- }
3691
- return coverage;
3692
- }
3693
- async function getMarketTradeFlowCoverage(params) {
3694
- const symbols = [
3695
- ...new Set(params.symbols.map((item) => item.toUpperCase()))
3696
- ];
3697
- if (!symbols.length) return /* @__PURE__ */ new Map();
3698
- await ensureBinanceMarketSchema();
3699
- const pool = getPool();
3700
- const res = await pool.query(
3701
- `
3702
- SELECT
3703
- symbol,
3704
- MIN(ts) AS first_ts,
3705
- MAX(ts) AS last_ts,
3706
- COUNT(*)::int AS rows
3707
- FROM market_trade_flow
3708
- WHERE symbol = ANY($1)
3709
- AND interval = $2
3710
- AND ts >= to_timestamp($3/1000.0)
3711
- AND ts <= to_timestamp($4/1000.0)
3712
- GROUP BY symbol
3713
- `,
3714
- [symbols, params.interval, params.startMs, params.endMs]
3715
- );
3716
- return new Map(
3717
- res.rows.map((row) => [
3718
- String(row.symbol).toUpperCase(),
3719
- {
3720
- firstMs: new Date(row.first_ts).getTime(),
3721
- lastMs: new Date(row.last_ts).getTime(),
3722
- rows: Number(row.rows) || 0
3723
- }
3724
- ])
3725
- );
3726
- }
3727
- var getTableRowCountIfExists = async (tableName) => {
3728
- const pool = getPool();
3729
- const exists = await pool.query("SELECT to_regclass($1) AS name", [
3730
- tableName
3731
- ]);
3732
- if (!exists.rows[0]?.name) return null;
3733
- const count = await pool.query(
3734
- `SELECT COUNT(*)::int AS rows FROM ${tableName}`
3735
- );
3736
- return Number(count.rows[0]?.rows ?? 0);
3737
- };
3738
- async function cleanupDeprecatedMarketContext(params = {}) {
3739
- const apply = Boolean(params.apply);
3740
- const pool = getPool();
3741
- const items = [];
3742
- const cleanupRows = async ({
3743
- tableName,
3744
- whereSql,
3745
- name
3746
- }) => {
3747
- const tableRows = await getTableRowCountIfExists(tableName);
3748
- if (tableRows == null) return;
3749
- const count = await pool.query(
3750
- `
3751
- SELECT COUNT(*)::int AS rows
3752
- FROM ${tableName}
3753
- WHERE ${whereSql}
3754
- `
3755
- );
3756
- const rows = Number(count.rows[0]?.rows ?? 0);
3757
- if (rows <= 0) return;
3758
- if (apply) {
3759
- await pool.query(
3760
- `
3761
- DELETE FROM ${tableName}
3762
- WHERE ${whereSql}
3763
- `
3764
- );
3765
- }
3766
- items.push({
3767
- kind: "rows",
3768
- name,
3769
- rows,
3770
- action: "delete_rows",
3771
- applied: apply
3772
- });
3773
- };
3774
- for (const tableName of ["market_order_book_depth", "onchain_flow_context"]) {
3775
- const rows = await getTableRowCountIfExists(tableName);
3776
- if (rows == null) continue;
3777
- if (apply) {
3778
- await pool.query(`DROP TABLE IF EXISTS ${tableName}`);
3779
- }
3780
- items.push({
3781
- kind: "table",
3782
- name: tableName,
3783
- rows,
3784
- action: "drop_table",
3785
- applied: apply
3786
- });
3787
- }
3788
- await cleanupRows({
3789
- tableName: "market_global_context",
3790
- whereSql: "source = 'coingecko_global'",
3791
- name: "market_global_context/source=coingecko_global"
3792
- });
3793
- await cleanupRows({
3794
- tableName: "market_global_context",
3795
- whereSql: "source = 'coinmarketcap_global_hourly'",
3796
- name: "market_global_context/source=coinmarketcap_global_hourly"
3797
- });
3798
- await cleanupRows({
3799
- tableName: "market_reference_asset_context",
3800
- whereSql: "source = 'coinmarketcap_reference_asset' AND interval = '1h'",
3801
- name: "market_reference_asset_context/source=coinmarketcap_reference_asset/interval=1h"
3802
- });
3803
- await cleanupRows({
3804
- tableName: "market_cmc_breadth_context",
3805
- whereSql: "source = 'coinmarketcap_market_breadth'",
3806
- name: "market_cmc_breadth_context/source=coinmarketcap_market_breadth"
3807
- });
3808
- await cleanupRows({
3809
- tableName: "market_context_backfill_coverage",
3810
- whereSql: "(source IN ('coinmarketcap_global_hourly', 'coinmarketcap_market_breadth') OR (source = 'coinmarketcap_reference_asset' AND interval = '1h'))",
3811
- name: "market_context_backfill_coverage/deprecated_cmc_sources"
3812
- });
3813
- return items;
3814
- }
3815
- async function getMarketBreadthCoverage(params) {
3816
- await ensureBinanceMarketSchema();
3817
- const pool = getPool();
3818
- const res = await pool.query(
3819
- `
3820
- SELECT
3821
- MIN(ts) AS first_ts,
3822
- MAX(ts) AS last_ts,
3823
- COUNT(*)::int AS rows,
3824
- COUNT(*) FILTER (
3825
- WHERE btc_alt_regime IS NOT NULL
3826
- AND btc_return_24h IS NOT NULL
3827
- AND alt_basket_return_24h IS NOT NULL
3828
- )::int AS btc_alt_metrics_rows
3829
- FROM market_breadth
3830
- WHERE universe = $1
3831
- AND interval = $2
3832
- AND ts >= to_timestamp($3/1000.0)
3833
- AND ts <= to_timestamp($4/1000.0)
3834
- `,
3835
- [params.universe, params.interval, params.startMs, params.endMs]
3836
- );
3837
- const row = res.rows[0];
3838
- if (!row?.first_ts || !row?.last_ts) return null;
3839
- return {
3840
- firstMs: new Date(row.first_ts).getTime(),
3841
- lastMs: new Date(row.last_ts).getTime(),
3842
- rows: Number(row.rows) || 0,
3843
- btcAltMetricsRows: Number(row.btc_alt_metrics_rows) || 0
3844
- };
3845
- }
3846
- async function getSpreadRangeForSymbols(symbols, interval, startMs, endMs) {
3847
- if (!symbols.length) {
3848
- return [];
3849
- }
3850
- await ensureSpreadSchema();
3851
- const pool = getPool();
3852
- const sql = `
3853
- SELECT symbol, interval, ts, binance_price, coinbase_price, spread
3854
- FROM market_spread
3855
- WHERE symbol = ANY($1)
3856
- AND interval = $2
3857
- AND ts >= to_timestamp($3/1000.0)
3858
- AND ts <= to_timestamp($4/1000.0)
3859
- ORDER BY symbol ASC, ts ASC
3860
- `;
3861
- const res = await pool.query(sql, [symbols, interval, startMs, endMs]);
3862
- return res.rows;
3863
- }
3864
- async function getSpreadSummary(hours = 24, limit = 500) {
3865
- await ensureSpreadSchema();
3866
- const pool = getPool();
3867
- const cappedHours = Math.max(1, Math.min(24 * 30, hours));
3868
- const cappedLimit = Math.max(50, Math.min(5e3, limit));
3869
- const rowsQ = await pool.query(
3870
- `
3871
- SELECT symbol, interval, ts, binance_price, coinbase_price, spread
3872
- FROM market_spread
3873
- WHERE ts >= now() - ($1 || ' hours')::interval
3874
- ORDER BY ts DESC
3875
- LIMIT $2
3876
- `,
3877
- [String(cappedHours), cappedLimit]
3878
- );
3879
- const aggQ = await pool.query(
3880
- `
3881
- SELECT
3882
- symbol,
3883
- interval,
3884
- COUNT(*)::int AS points,
3885
- MAX(ts) AS last_ts,
3886
- AVG(spread) AS avg_spread,
3887
- STDDEV_POP(spread) AS std_spread
3888
- FROM market_spread
3889
- WHERE ts >= now() - ($1 || ' hours')::interval
3890
- GROUP BY symbol, interval
3891
- ORDER BY points DESC, symbol ASC
3892
- LIMIT 500
3893
- `,
3894
- [String(cappedHours)]
3895
- );
3896
- return {
3897
- rows: rowsQ.rows,
3898
- aggregates: aggQ.rows,
3899
- hours: cappedHours
3900
- };
3901
- }
3902
- async function getCandlesRange(provider, symbol, interval, startMs, endMs) {
3903
- await ensureCandlesSchema();
3904
- const pool = getPool();
3905
- const normalizedProvider = normalizeCandleProvider(provider);
3906
- const normalizedSymbol = normalizeCandleSymbol(symbol);
3907
- const sql = `
3908
- SELECT symbol, interval, ts,
3909
- open, high, low, close, volume, turnover,
3910
- taker_buy_base_volume AS "takerBuyBaseVolume",
3911
- taker_buy_quote_volume AS "takerBuyQuoteVolume",
3912
- taker_sell_base_volume AS "takerSellBaseVolume",
3913
- taker_sell_quote_volume AS "takerSellQuoteVolume"
3914
- FROM candles
3915
- WHERE provider = $1 AND symbol = $2 AND interval = $3
3916
- AND ts >= to_timestamp($4/1000.0)
3917
- AND ts <= to_timestamp($5/1000.0)
3918
- ORDER BY ts ASC
3919
- `;
3920
- const res = await pool.query(sql, [
3921
- normalizedProvider,
3922
- normalizedSymbol,
3923
- interval,
3924
- startMs,
3925
- endMs
3926
- ]);
3927
- return res.rows;
3928
- }
3929
- async function getDataEdges(provider, symbol, interval) {
3930
- await ensureCandlesSchema();
3931
- const pool = getPool();
3932
- const normalizedProvider = normalizeCandleProvider(provider);
3933
- const normalizedSymbol = normalizeCandleSymbol(symbol);
3934
- const sqlMin = `
3935
- SELECT extract(epoch from ts)*1000 AS ms
3936
- FROM candles
3937
- WHERE provider=$1 AND symbol=$2 AND interval=$3
3938
- ORDER BY ts ASC
3939
- LIMIT 1
3940
- `;
3941
- const sqlMax = `
3942
- SELECT extract(epoch from ts)*1000 AS ms
3943
- FROM candles
3944
- WHERE provider=$1 AND symbol=$2 AND interval=$3
3945
- ORDER BY ts DESC
3946
- LIMIT 1
3947
- `;
3948
- const [minQ, maxQ] = await Promise.all([
3949
- pool.query(sqlMin, [normalizedProvider, normalizedSymbol, interval]),
3950
- pool.query(sqlMax, [normalizedProvider, normalizedSymbol, interval])
3951
- ]);
3952
- const minRaw = minQ.rows[0]?.ms;
3953
- const maxRaw = maxQ.rows[0]?.ms;
3954
- const min = Number.isFinite(Number(minRaw)) ? Number(minRaw) : void 0;
3955
- const max = Number.isFinite(Number(maxRaw)) ? Number(maxRaw) : void 0;
3956
- return { min, max };
3957
- }
3958
- async function getDataEdgesForSymbols(provider, symbols, interval) {
3959
- const normalizedSymbols = [
3960
- ...new Set(symbols.map(normalizeCandleSymbol).filter(Boolean))
3961
- ];
3962
- const result = /* @__PURE__ */ new Map();
3963
- for (const symbol of normalizedSymbols) {
3964
- result.set(symbol, {});
3965
- }
3966
- if (!normalizedSymbols.length) {
3967
- return result;
3968
- }
3969
- await ensureCandlesSchema();
3970
- const pool = getPool();
3971
- const normalizedProvider = normalizeCandleProvider(provider);
3972
- const sql = `
3973
- WITH requested(symbol) AS (
3974
- SELECT unnest($2::text[])
3975
- )
3976
- SELECT
3977
- r.symbol,
3978
- (
3979
- SELECT extract(epoch from c.ts)*1000
3980
- FROM candles c
3981
- WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
3982
- ORDER BY c.ts ASC
3983
- LIMIT 1
3984
- ) AS min_ms,
3985
- (
3986
- SELECT extract(epoch from c.ts)*1000
3987
- FROM candles c
3988
- WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
3989
- ORDER BY c.ts DESC
3990
- LIMIT 1
3991
- ) AS max_ms
3992
- FROM requested r
3993
- `;
3994
- const response = await pool.query(sql, [
3995
- normalizedProvider,
3996
- normalizedSymbols,
3997
- interval
3998
- ]);
3999
- for (const row of response.rows) {
4000
- const symbol = normalizeCandleSymbol(String(row.symbol || ""));
4001
- if (!symbol) continue;
4002
- const min = row.min_ms == null ? NaN : Number(row.min_ms);
4003
- const max = row.max_ms == null ? NaN : Number(row.max_ms);
4004
- result.set(symbol, {
4005
- ...Number.isFinite(min) ? { min } : {},
4006
- ...Number.isFinite(max) ? { max } : {}
4007
- });
4008
- }
4009
- return result;
4010
- }
4011
- async function waitForDbReady(attempts = 20, delayMs = 1e3) {
4012
- const pool = getPool();
4013
- let lastError;
4014
- for (let i = 0; i < attempts; i++) {
4015
- try {
4016
- await pool.query("SELECT 1");
4017
- return;
4018
- } catch (e) {
4019
- lastError = e;
4020
- await new Promise((r) => setTimeout(r, delayMs));
4021
- }
4022
- }
4023
- throw lastError;
4024
- }
4025
- async function deleteCandles(provider, symbol, interval) {
4026
- const pool = getPool();
4027
- const normalizedProvider = normalizeCandleProvider(provider);
4028
- const normalizedSymbol = normalizeCandleSymbol(symbol);
4029
- const sql = `
4030
- DELETE FROM candles
4031
- WHERE provider = $1 AND symbol = $2 AND interval = $3
4032
- `;
4033
- await pool.query(sql, [normalizedProvider, normalizedSymbol, interval]);
4034
- }
4035
- async function findContinuityGap(provider, symbol, interval) {
4036
- const pool = getPool();
4037
- const normalizedProvider = normalizeCandleProvider(provider);
4038
- const normalizedSymbol = normalizeCandleSymbol(symbol);
4039
- const expectedSeconds = interval * 60;
4040
- const sql = `
4041
- WITH ordered AS (
4042
- SELECT
4043
- ts,
4044
- LAG(ts) OVER (ORDER BY ts) AS prev_ts
4045
- FROM candles
4046
- WHERE provider = $1 AND symbol = $2 AND interval = $3
4047
- )
4048
- SELECT
4049
- ts,
4050
- prev_ts,
4051
- EXTRACT(EPOCH FROM (ts - prev_ts))::int AS diff_seconds
4052
- FROM ordered
4053
- WHERE prev_ts IS NOT NULL
4054
- AND EXTRACT(EPOCH FROM (ts - prev_ts))::int <> $4
4055
- ORDER BY ts ASC
4056
- LIMIT 1
4057
- `;
4058
- const res = await pool.query(sql, [
4059
- normalizedProvider,
4060
- normalizedSymbol,
4061
- interval,
4062
- expectedSeconds
4063
- ]);
4064
- const row = res.rows[0];
4065
- if (!row) return null;
4066
- return {
4067
- ts: new Date(row.ts).getTime(),
4068
- prevTs: new Date(row.prev_ts).getTime(),
4069
- diffSeconds: row.diff_seconds
4070
- };
4071
- }
2
+ applyDerivativesMetricCoverage,
3
+ getDerivativesBackfillCoverage,
4
+ getDerivativesDataEdgesForSymbols,
5
+ getDerivativesMetricCoverage,
6
+ getDerivativesRangeForSymbols,
7
+ getDerivativesSummary,
8
+ getDerivativesWindow,
9
+ upsertDerivatives,
10
+ upsertDerivativesBackfillCoverage
11
+ } from "./chunk-SZQB7ER5.mjs";
12
+ import {
13
+ getHyperliquidWhaleCoverageSeriesRows,
14
+ getHyperliquidWhaleFlowAggregate,
15
+ getHyperliquidWhaleFlowSeriesRows,
16
+ getHyperliquidWhaleWalletCoverage,
17
+ hasHyperliquidWhaleBackfillCoverage,
18
+ rebuildHyperliquidWhaleCoverageRows,
19
+ rebuildHyperliquidWhaleFlowRows,
20
+ upsertHyperliquidWhaleCoverageRows,
21
+ upsertHyperliquidWhaleFlowRows,
22
+ upsertHyperliquidWhaleTradeEvents,
23
+ upsertHyperliquidWhaleWalletCoverage
24
+ } from "./chunk-NWXFWTWU.mjs";
25
+ import {
26
+ cleanupDeprecatedMarketContext,
27
+ getLatestMarketBreadth,
28
+ getLatestMarketCmcExchangeLiquidityContext,
29
+ getLatestMarketCmcFearGreedContext,
30
+ getLatestMarketCmcIndexContexts,
31
+ getLatestMarketGlobalContext,
32
+ getLatestMarketReferenceAssetContexts,
33
+ getLatestMarketTradeFlow,
34
+ getMarketBreadthCoverage,
35
+ getMarketCmcExchangeLiquidityContextCoverage,
36
+ getMarketCmcFearGreedContextCoverage,
37
+ getMarketCmcIndexContextCoverage,
38
+ getMarketContextBackfillCoverage,
39
+ getMarketGlobalContextCoverage,
40
+ getMarketReferenceAssetContextCoverage,
41
+ getMarketTradeFlowCoverage,
42
+ upsertMarketBreadthRows,
43
+ upsertMarketCmcExchangeLiquidityContextRows,
44
+ upsertMarketCmcFearGreedContextRows,
45
+ upsertMarketCmcIndexContextRows,
46
+ upsertMarketContextBackfillCoverage,
47
+ upsertMarketGlobalContextRows,
48
+ upsertMarketReferenceAssetContextRows,
49
+ upsertMarketTradeFlowRows
50
+ } from "./chunk-DFMKDB2R.mjs";
51
+ import {
52
+ getSpreadRangeForSymbols,
53
+ getSpreadSummary,
54
+ upsertSpreadRows
55
+ } from "./chunk-2CZREG43.mjs";
56
+ import {
57
+ deleteCandles,
58
+ findContinuityGap,
59
+ getCandlesRange,
60
+ getDataEdges,
61
+ getDataEdgesForSymbols,
62
+ toRows,
63
+ upsertCandles,
64
+ waitForDbReady
65
+ } from "./chunk-YVIHTUV5.mjs";
66
+ import {
67
+ closeTimescalePool,
68
+ configureTimescaleMarketContextSchemaMode,
69
+ ensureBinanceMarketSchema,
70
+ ensureCoinMarketCapContextSchema,
71
+ ensureDerivativesSchema,
72
+ ensureHyperliquidWhaleSchema,
73
+ ensureMarketContextSchemas
74
+ } from "./chunk-I2J6YDBD.mjs";
4072
75
  export {
4073
76
  applyDerivativesMetricCoverage,
4074
77
  cleanupDeprecatedMarketContext,