@tradejs/infra 2.0.21 → 3.0.0

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