@tradejs/infra 1.0.9 → 1.0.11

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.
@@ -7,23 +7,120 @@ var getPool = () => {
7
7
  const user = process.env.PG_USER || "app";
8
8
  const password = String(process.env.PG_PASSWORD ?? "app");
9
9
  const database = process.env.PG_DATABASE || process.env.PG_DB || "app";
10
+ const max = Number(process.env.PG_POOL_MAX ?? 10);
11
+ const connectionTimeoutMillis = Number(
12
+ process.env.PG_CONNECTION_TIMEOUT_MS ?? 3e4
13
+ );
10
14
  global.__pgPool__ = new Pool({
11
15
  host,
12
16
  port,
13
17
  user,
14
18
  password,
15
19
  database,
16
- max: 10,
20
+ max: Number.isFinite(max) && max > 0 ? Math.floor(max) : 10,
17
21
  idleTimeoutMillis: 3e4,
18
- connectionTimeoutMillis: 5e3
22
+ connectionTimeoutMillis: Number.isFinite(connectionTimeoutMillis) && connectionTimeoutMillis > 0 ? Math.floor(connectionTimeoutMillis) : 3e4
19
23
  });
20
24
  }
21
25
  return global.__pgPool__;
22
26
  };
27
+ var candlesSchemaReady = false;
23
28
  var derivativesSchemaReady = false;
24
29
  var spreadSchemaReady = false;
30
+ var binanceMarketSchemaReady = false;
31
+ var candlesSchemaReadyPromise = null;
32
+ var derivativesSchemaReadyPromise = null;
33
+ var spreadSchemaReadyPromise = null;
34
+ var binanceMarketSchemaReadyPromise = null;
35
+ var closeTimescalePool = async () => {
36
+ const pool = global.__pgPool__;
37
+ if (!pool) {
38
+ return;
39
+ }
40
+ global.__pgPool__ = void 0;
41
+ candlesSchemaReady = false;
42
+ derivativesSchemaReady = false;
43
+ spreadSchemaReady = false;
44
+ binanceMarketSchemaReady = false;
45
+ candlesSchemaReadyPromise = null;
46
+ derivativesSchemaReadyPromise = null;
47
+ spreadSchemaReadyPromise = null;
48
+ binanceMarketSchemaReadyPromise = null;
49
+ await pool.end();
50
+ };
51
+ var CANDLES_SCHEMA_LOCK_KEY = 61e4;
52
+ var DERIVATIVES_SCHEMA_LOCK_KEY = 610001;
53
+ var SPREAD_SCHEMA_LOCK_KEY = 610002;
54
+ var BINANCE_MARKET_SCHEMA_LOCK_KEY = 610003;
55
+ var PG_SAFE_MAX_BIND_PARAMS = 3e4;
25
56
  var normalizeCandleProvider = (provider) => String(provider || "").trim().toLowerCase();
26
57
  var normalizeCandleSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
58
+ var getSafeBulkInsertRows = (columnsCount) => Math.max(1, Math.floor(PG_SAFE_MAX_BIND_PARAMS / columnsCount));
59
+ var withSchemaLock = async (lockKey, work) => {
60
+ const pool = getPool();
61
+ await pool.query("SELECT pg_advisory_lock($1)", [lockKey]);
62
+ try {
63
+ await work();
64
+ } finally {
65
+ await pool.query("SELECT pg_advisory_unlock($1)", [lockKey]);
66
+ }
67
+ };
68
+ var ensureCandlesSchema = async () => {
69
+ if (candlesSchemaReady) return;
70
+ if (candlesSchemaReadyPromise) {
71
+ await candlesSchemaReadyPromise;
72
+ return;
73
+ }
74
+ candlesSchemaReadyPromise = withSchemaLock(
75
+ CANDLES_SCHEMA_LOCK_KEY,
76
+ async () => {
77
+ const pool = getPool();
78
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
79
+ await pool.query(`
80
+ CREATE TABLE IF NOT EXISTS candles (
81
+ provider text NOT NULL DEFAULT 'bybit',
82
+ symbol text NOT NULL,
83
+ interval integer NOT NULL,
84
+ ts timestamptz NOT NULL,
85
+ open double precision NOT NULL,
86
+ high double precision NOT NULL,
87
+ low double precision NOT NULL,
88
+ close double precision NOT NULL,
89
+ volume double precision,
90
+ turnover double precision,
91
+ taker_buy_base_volume double precision,
92
+ taker_buy_quote_volume double precision,
93
+ taker_sell_base_volume double precision,
94
+ taker_sell_quote_volume double precision,
95
+ PRIMARY KEY (provider, symbol, interval, ts)
96
+ )
97
+ `);
98
+ await pool.query(`
99
+ SELECT create_hypertable(
100
+ 'candles',
101
+ 'ts',
102
+ if_not_exists => TRUE,
103
+ chunk_time_interval => interval '7 days'
104
+ )
105
+ `);
106
+ await pool.query(`
107
+ CREATE INDEX IF NOT EXISTS candles_provider_symbol_interval_ts_idx
108
+ ON candles (provider, symbol, interval, ts DESC)
109
+ `);
110
+ await pool.query(`
111
+ ALTER TABLE candles
112
+ ADD COLUMN IF NOT EXISTS taker_buy_base_volume double precision,
113
+ ADD COLUMN IF NOT EXISTS taker_buy_quote_volume double precision,
114
+ ADD COLUMN IF NOT EXISTS taker_sell_base_volume double precision,
115
+ ADD COLUMN IF NOT EXISTS taker_sell_quote_volume double precision
116
+ `);
117
+ candlesSchemaReady = true;
118
+ }
119
+ ).finally(() => {
120
+ candlesSchemaReadyPromise = null;
121
+ });
122
+ await candlesSchemaReadyPromise;
123
+ };
27
124
  var toRows = (provider, symbol, interval, data) => {
28
125
  const normalizedProvider = normalizeCandleProvider(provider);
29
126
  if (!normalizedProvider) {
@@ -41,11 +138,16 @@ var toRows = (provider, symbol, interval, data) => {
41
138
  low: i.low,
42
139
  close: i.close,
43
140
  volume: i.volume ?? null,
44
- turnover: i.turnover ?? null
141
+ turnover: i.turnover ?? null,
142
+ takerBuyBaseVolume: i.takerBuyBaseVolume ?? null,
143
+ takerBuyQuoteVolume: i.takerBuyQuoteVolume ?? null,
144
+ takerSellBaseVolume: i.takerSellBaseVolume ?? null,
145
+ takerSellQuoteVolume: i.takerSellQuoteVolume ?? null
45
146
  }));
46
147
  };
47
148
  async function upsertCandles(rows) {
48
149
  if (!rows.length) return;
150
+ await ensureCandlesSchema();
49
151
  const pool = getPool();
50
152
  const cols = [
51
153
  "provider",
@@ -57,7 +159,11 @@ async function upsertCandles(rows) {
57
159
  "low",
58
160
  "close",
59
161
  "volume",
60
- "turnover"
162
+ "turnover",
163
+ "taker_buy_base_volume",
164
+ "taker_buy_quote_volume",
165
+ "taker_sell_base_volume",
166
+ "taker_sell_quote_volume"
61
167
  ];
62
168
  const maxRows = Math.floor(65535 / cols.length);
63
169
  if (rows.length > maxRows) {
@@ -79,7 +185,11 @@ async function upsertCandles(rows) {
79
185
  r.low,
80
186
  r.close,
81
187
  r.volume ?? null,
82
- r.turnover ?? null
188
+ r.turnover ?? null,
189
+ r.takerBuyBaseVolume ?? null,
190
+ r.takerBuyQuoteVolume ?? null,
191
+ r.takerSellBaseVolume ?? null,
192
+ r.takerSellQuoteVolume ?? null
83
193
  ]);
84
194
  const sql = `
85
195
  INSERT INTO candles (${cols.join(",")})
@@ -90,7 +200,11 @@ async function upsertCandles(rows) {
90
200
  low = EXCLUDED.low,
91
201
  close = EXCLUDED.close,
92
202
  volume = COALESCE(EXCLUDED.volume, candles.volume),
93
- turnover = COALESCE(EXCLUDED.turnover, candles.turnover)
203
+ turnover = COALESCE(EXCLUDED.turnover, candles.turnover),
204
+ taker_buy_base_volume = COALESCE(EXCLUDED.taker_buy_base_volume, candles.taker_buy_base_volume),
205
+ taker_buy_quote_volume = COALESCE(EXCLUDED.taker_buy_quote_volume, candles.taker_buy_quote_volume),
206
+ taker_sell_base_volume = COALESCE(EXCLUDED.taker_sell_base_volume, candles.taker_sell_base_volume),
207
+ taker_sell_quote_volume = COALESCE(EXCLUDED.taker_sell_quote_volume, candles.taker_sell_quote_volume)
94
208
  `;
95
209
  const client = await pool.connect();
96
210
  try {
@@ -106,67 +220,389 @@ async function upsertCandles(rows) {
106
220
  }
107
221
  var ensureDerivativesSchema = async () => {
108
222
  if (derivativesSchemaReady) return;
223
+ if (derivativesSchemaReadyPromise) {
224
+ await derivativesSchemaReadyPromise;
225
+ return;
226
+ }
109
227
  const pool = getPool();
110
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
111
- await pool.query(`
112
- CREATE TABLE IF NOT EXISTS derivatives_market (
113
- symbol text NOT NULL,
114
- interval text NOT NULL,
115
- ts timestamptz NOT NULL,
116
- open_interest double precision,
117
- funding_rate double precision,
118
- liq_long double precision,
119
- liq_short double precision,
120
- liq_total double precision,
121
- source text,
122
- ingested_at timestamptz NOT NULL DEFAULT now(),
123
- PRIMARY KEY (symbol, interval, ts)
124
- )
125
- `);
126
- await pool.query(`
127
- SELECT create_hypertable(
128
- 'derivatives_market',
129
- 'ts',
130
- if_not_exists => TRUE,
131
- chunk_time_interval => interval '14 days'
132
- )
133
- `);
134
- await pool.query(`
135
- CREATE INDEX IF NOT EXISTS derivatives_market_symbol_tf_ts_idx
136
- ON derivatives_market (symbol, interval, ts DESC)
137
- `);
138
- derivativesSchemaReady = true;
228
+ derivativesSchemaReadyPromise = withSchemaLock(
229
+ DERIVATIVES_SCHEMA_LOCK_KEY,
230
+ async () => {
231
+ if (derivativesSchemaReady) return;
232
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
233
+ await pool.query(`
234
+ CREATE TABLE IF NOT EXISTS derivatives_market (
235
+ symbol text NOT NULL,
236
+ interval text NOT NULL,
237
+ ts timestamptz NOT NULL,
238
+ open_interest double precision,
239
+ funding_rate double precision,
240
+ liq_long double precision,
241
+ liq_short double precision,
242
+ liq_total double precision,
243
+ source text,
244
+ ingested_at timestamptz NOT NULL DEFAULT now(),
245
+ PRIMARY KEY (symbol, interval, ts)
246
+ )
247
+ `);
248
+ await pool.query(`
249
+ SELECT create_hypertable(
250
+ 'derivatives_market',
251
+ 'ts',
252
+ if_not_exists => TRUE,
253
+ chunk_time_interval => interval '14 days'
254
+ )
255
+ `);
256
+ await pool.query(`
257
+ CREATE INDEX IF NOT EXISTS derivatives_market_symbol_tf_ts_idx
258
+ ON derivatives_market (symbol, interval, ts DESC)
259
+ `);
260
+ await pool.query(`
261
+ CREATE TABLE IF NOT EXISTS derivatives_backfill_coverage (
262
+ source text NOT NULL,
263
+ symbol text NOT NULL,
264
+ interval text NOT NULL,
265
+ from_ts timestamptz NOT NULL,
266
+ to_ts timestamptz NOT NULL,
267
+ rows_count integer NOT NULL DEFAULT 0,
268
+ checked_at timestamptz NOT NULL DEFAULT now(),
269
+ PRIMARY KEY (source, symbol, interval, from_ts, to_ts)
270
+ )
271
+ `);
272
+ await pool.query(`
273
+ CREATE INDEX IF NOT EXISTS derivatives_backfill_coverage_lookup_idx
274
+ ON derivatives_backfill_coverage (source, symbol, interval, from_ts, to_ts)
275
+ `);
276
+ derivativesSchemaReady = true;
277
+ }
278
+ ).finally(() => {
279
+ derivativesSchemaReadyPromise = null;
280
+ });
281
+ await derivativesSchemaReadyPromise;
139
282
  };
140
283
  var ensureSpreadSchema = async () => {
141
284
  if (spreadSchemaReady) return;
285
+ if (spreadSchemaReadyPromise) {
286
+ await spreadSchemaReadyPromise;
287
+ return;
288
+ }
142
289
  const pool = getPool();
143
- await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
144
- await pool.query(`
145
- CREATE TABLE IF NOT EXISTS market_spread (
146
- symbol text NOT NULL,
147
- interval text NOT NULL,
148
- ts timestamptz NOT NULL,
149
- binance_price double precision,
150
- coinbase_price double precision,
151
- spread double precision,
152
- source text,
153
- ingested_at timestamptz NOT NULL DEFAULT now(),
154
- PRIMARY KEY (symbol, interval, ts)
155
- )
156
- `);
157
- await pool.query(`
158
- SELECT create_hypertable(
159
- 'market_spread',
160
- 'ts',
161
- if_not_exists => TRUE,
162
- chunk_time_interval => interval '14 days'
163
- )
164
- `);
165
- await pool.query(`
166
- CREATE INDEX IF NOT EXISTS market_spread_symbol_tf_ts_idx
167
- ON market_spread (symbol, interval, ts DESC)
168
- `);
169
- spreadSchemaReady = true;
290
+ spreadSchemaReadyPromise = withSchemaLock(
291
+ SPREAD_SCHEMA_LOCK_KEY,
292
+ async () => {
293
+ if (spreadSchemaReady) return;
294
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
295
+ await pool.query(`
296
+ CREATE TABLE IF NOT EXISTS market_spread (
297
+ symbol text NOT NULL,
298
+ interval text NOT NULL,
299
+ ts timestamptz NOT NULL,
300
+ binance_price double precision,
301
+ coinbase_price double precision,
302
+ spread double precision,
303
+ source text,
304
+ ingested_at timestamptz NOT NULL DEFAULT now(),
305
+ PRIMARY KEY (symbol, interval, ts)
306
+ )
307
+ `);
308
+ await pool.query(`
309
+ SELECT create_hypertable(
310
+ 'market_spread',
311
+ 'ts',
312
+ if_not_exists => TRUE,
313
+ chunk_time_interval => interval '14 days'
314
+ )
315
+ `);
316
+ await pool.query(`
317
+ CREATE INDEX IF NOT EXISTS market_spread_symbol_tf_ts_idx
318
+ ON market_spread (symbol, interval, ts DESC)
319
+ `);
320
+ spreadSchemaReady = true;
321
+ }
322
+ ).finally(() => {
323
+ spreadSchemaReadyPromise = null;
324
+ });
325
+ await spreadSchemaReadyPromise;
326
+ };
327
+ var ensureBinanceMarketSchema = async () => {
328
+ if (binanceMarketSchemaReady) return;
329
+ if (binanceMarketSchemaReadyPromise) {
330
+ await binanceMarketSchemaReadyPromise;
331
+ return;
332
+ }
333
+ const pool = getPool();
334
+ binanceMarketSchemaReadyPromise = withSchemaLock(
335
+ BINANCE_MARKET_SCHEMA_LOCK_KEY,
336
+ async () => {
337
+ if (binanceMarketSchemaReady) return;
338
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
339
+ await pool.query(`
340
+ CREATE TABLE IF NOT EXISTS market_trade_flow (
341
+ symbol text NOT NULL,
342
+ interval text NOT NULL,
343
+ ts timestamptz NOT NULL,
344
+ trades integer NOT NULL,
345
+ buy_base_volume double precision,
346
+ sell_base_volume double precision,
347
+ buy_quote_volume double precision,
348
+ sell_quote_volume double precision,
349
+ net_base_delta double precision,
350
+ net_quote_delta double precision,
351
+ buy_pressure_pct double precision,
352
+ source text,
353
+ ingested_at timestamptz NOT NULL DEFAULT now(),
354
+ PRIMARY KEY (symbol, interval, ts)
355
+ )
356
+ `);
357
+ await pool.query(`
358
+ SELECT create_hypertable(
359
+ 'market_trade_flow',
360
+ 'ts',
361
+ if_not_exists => TRUE,
362
+ chunk_time_interval => interval '7 days'
363
+ )
364
+ `);
365
+ await pool.query(`
366
+ CREATE INDEX IF NOT EXISTS market_trade_flow_symbol_tf_ts_idx
367
+ ON market_trade_flow (symbol, interval, ts DESC)
368
+ `);
369
+ await pool.query(`
370
+ CREATE TABLE IF NOT EXISTS market_breadth (
371
+ universe text NOT NULL,
372
+ interval text NOT NULL,
373
+ ts timestamptz NOT NULL,
374
+ symbols_count integer NOT NULL,
375
+ advancers integer NOT NULL,
376
+ decliners integer NOT NULL,
377
+ unchanged integer NOT NULL,
378
+ advance_decline_ratio double precision,
379
+ pct_above_ma20 double precision,
380
+ pct_above_ma50 double precision,
381
+ equal_weighted_return double precision,
382
+ volume_weighted_return double precision,
383
+ dispersion double precision,
384
+ btc_return_1h double precision,
385
+ btc_return_4h double precision,
386
+ btc_return_24h double precision,
387
+ alt_basket_return_1h double precision,
388
+ alt_basket_return_4h double precision,
389
+ alt_basket_return_24h double precision,
390
+ btc_vs_alt_return_1h double precision,
391
+ btc_vs_alt_return_4h double precision,
392
+ btc_vs_alt_return_24h double precision,
393
+ btc_turnover_share_1h double precision,
394
+ btc_turnover_share_24h double precision,
395
+ btc_turnover_share_change_24h double precision,
396
+ alt_vol_to_btc_vol_24h double precision,
397
+ alt_dispersion_24h double precision,
398
+ btc_alt_regime text,
399
+ source text,
400
+ ingested_at timestamptz NOT NULL DEFAULT now(),
401
+ PRIMARY KEY (universe, interval, ts)
402
+ )
403
+ `);
404
+ await pool.query(`
405
+ ALTER TABLE market_breadth
406
+ ADD COLUMN IF NOT EXISTS btc_return_1h double precision,
407
+ ADD COLUMN IF NOT EXISTS btc_return_4h double precision,
408
+ ADD COLUMN IF NOT EXISTS btc_return_24h double precision,
409
+ ADD COLUMN IF NOT EXISTS alt_basket_return_1h double precision,
410
+ ADD COLUMN IF NOT EXISTS alt_basket_return_4h double precision,
411
+ ADD COLUMN IF NOT EXISTS alt_basket_return_24h double precision,
412
+ ADD COLUMN IF NOT EXISTS btc_vs_alt_return_1h double precision,
413
+ ADD COLUMN IF NOT EXISTS btc_vs_alt_return_4h double precision,
414
+ ADD COLUMN IF NOT EXISTS btc_vs_alt_return_24h double precision,
415
+ ADD COLUMN IF NOT EXISTS btc_turnover_share_1h double precision,
416
+ ADD COLUMN IF NOT EXISTS btc_turnover_share_24h double precision,
417
+ ADD COLUMN IF NOT EXISTS btc_turnover_share_change_24h double precision,
418
+ ADD COLUMN IF NOT EXISTS alt_vol_to_btc_vol_24h double precision,
419
+ ADD COLUMN IF NOT EXISTS alt_dispersion_24h double precision,
420
+ ADD COLUMN IF NOT EXISTS btc_alt_regime text
421
+ `);
422
+ await pool.query(`
423
+ SELECT create_hypertable(
424
+ 'market_breadth',
425
+ 'ts',
426
+ if_not_exists => TRUE,
427
+ chunk_time_interval => interval '14 days'
428
+ )
429
+ `);
430
+ await pool.query(`
431
+ CREATE INDEX IF NOT EXISTS market_breadth_universe_tf_ts_idx
432
+ ON market_breadth (universe, interval, ts DESC)
433
+ `);
434
+ await pool.query(`
435
+ CREATE TABLE IF NOT EXISTS market_global_context (
436
+ source text NOT NULL,
437
+ ts timestamptz NOT NULL,
438
+ updated_at_ts timestamptz,
439
+ active_cryptocurrencies integer,
440
+ active_exchanges integer,
441
+ active_market_pairs integer,
442
+ markets integer,
443
+ total_market_cap_usd double precision,
444
+ total_volume_usd double precision,
445
+ total_volume_reported_usd double precision,
446
+ btc_dominance_pct double precision,
447
+ eth_dominance_pct double precision,
448
+ alt_market_cap_usd double precision,
449
+ alt_volume_usd double precision,
450
+ alt_volume_reported_usd double precision,
451
+ btc_to_alt_market_cap_ratio double precision,
452
+ market_cap_change_pct_24h_usd double precision,
453
+ ingested_at timestamptz NOT NULL DEFAULT now(),
454
+ PRIMARY KEY (source, ts)
455
+ )
456
+ `);
457
+ await pool.query(`
458
+ SELECT create_hypertable(
459
+ 'market_global_context',
460
+ 'ts',
461
+ if_not_exists => TRUE,
462
+ chunk_time_interval => interval '30 days'
463
+ )
464
+ `);
465
+ await pool.query(`
466
+ CREATE INDEX IF NOT EXISTS market_global_context_source_ts_idx
467
+ ON market_global_context (source, ts DESC)
468
+ `);
469
+ await pool.query(`
470
+ ALTER TABLE market_global_context
471
+ ADD COLUMN IF NOT EXISTS active_exchanges integer,
472
+ ADD COLUMN IF NOT EXISTS active_market_pairs integer,
473
+ ADD COLUMN IF NOT EXISTS total_volume_reported_usd double precision,
474
+ ADD COLUMN IF NOT EXISTS alt_volume_usd double precision,
475
+ ADD COLUMN IF NOT EXISTS alt_volume_reported_usd double precision
476
+ `);
477
+ await pool.query(`
478
+ CREATE TABLE IF NOT EXISTS market_reference_asset_context (
479
+ source text NOT NULL,
480
+ symbol text NOT NULL,
481
+ cmc_id integer NOT NULL,
482
+ interval text NOT NULL,
483
+ ts timestamptz NOT NULL,
484
+ open_usd double precision,
485
+ high_usd double precision,
486
+ low_usd double precision,
487
+ close_usd double precision,
488
+ volume_usd double precision,
489
+ market_cap_usd double precision,
490
+ ingested_at timestamptz NOT NULL DEFAULT now(),
491
+ PRIMARY KEY (source, symbol, interval, ts)
492
+ )
493
+ `);
494
+ await pool.query(`
495
+ SELECT create_hypertable(
496
+ 'market_reference_asset_context',
497
+ 'ts',
498
+ if_not_exists => TRUE,
499
+ chunk_time_interval => interval '30 days'
500
+ )
501
+ `);
502
+ await pool.query(`
503
+ CREATE INDEX IF NOT EXISTS market_reference_asset_context_lookup_idx
504
+ ON market_reference_asset_context (source, symbol, interval, ts DESC)
505
+ `);
506
+ await pool.query(`
507
+ CREATE TABLE IF NOT EXISTS market_cmc_exchange_liquidity_context (
508
+ source text NOT NULL,
509
+ interval text NOT NULL,
510
+ ts timestamptz NOT NULL,
511
+ exchanges_count integer NOT NULL,
512
+ total_volume_usd double precision,
513
+ binance_volume_usd double precision,
514
+ binance_volume_share double precision,
515
+ top_exchange_volume_share double precision,
516
+ liquidity_regime text,
517
+ ingested_at timestamptz NOT NULL DEFAULT now(),
518
+ PRIMARY KEY (source, interval, ts)
519
+ )
520
+ `);
521
+ await pool.query(`
522
+ SELECT create_hypertable(
523
+ 'market_cmc_exchange_liquidity_context',
524
+ 'ts',
525
+ if_not_exists => TRUE,
526
+ chunk_time_interval => interval '30 days'
527
+ )
528
+ `);
529
+ await pool.query(`
530
+ CREATE INDEX IF NOT EXISTS market_cmc_exchange_liquidity_context_lookup_idx
531
+ ON market_cmc_exchange_liquidity_context (source, interval, ts DESC)
532
+ `);
533
+ await pool.query(`
534
+ CREATE TABLE IF NOT EXISTS market_cmc_fear_greed_context (
535
+ source text NOT NULL,
536
+ interval text NOT NULL,
537
+ ts timestamptz NOT NULL,
538
+ value integer NOT NULL,
539
+ classification text NOT NULL,
540
+ sentiment_regime text NOT NULL,
541
+ ingested_at timestamptz NOT NULL DEFAULT now(),
542
+ PRIMARY KEY (source, interval, ts)
543
+ )
544
+ `);
545
+ await pool.query(`
546
+ SELECT create_hypertable(
547
+ 'market_cmc_fear_greed_context',
548
+ 'ts',
549
+ if_not_exists => TRUE,
550
+ chunk_time_interval => interval '30 days'
551
+ )
552
+ `);
553
+ await pool.query(`
554
+ CREATE INDEX IF NOT EXISTS market_cmc_fear_greed_context_lookup_idx
555
+ ON market_cmc_fear_greed_context (source, interval, ts DESC)
556
+ `);
557
+ await pool.query(`
558
+ CREATE TABLE IF NOT EXISTS market_cmc_index_context (
559
+ source text NOT NULL,
560
+ index_slug text NOT NULL,
561
+ interval text NOT NULL,
562
+ ts timestamptz NOT NULL,
563
+ value double precision NOT NULL,
564
+ constituents_count integer,
565
+ top_constituent_symbol text,
566
+ top_constituent_weight_pct double precision,
567
+ constituents jsonb,
568
+ ingested_at timestamptz NOT NULL DEFAULT now(),
569
+ PRIMARY KEY (source, index_slug, interval, ts)
570
+ )
571
+ `);
572
+ await pool.query(`
573
+ SELECT create_hypertable(
574
+ 'market_cmc_index_context',
575
+ 'ts',
576
+ if_not_exists => TRUE,
577
+ chunk_time_interval => interval '30 days'
578
+ )
579
+ `);
580
+ await pool.query(`
581
+ CREATE INDEX IF NOT EXISTS market_cmc_index_context_lookup_idx
582
+ ON market_cmc_index_context (source, index_slug, interval, ts DESC)
583
+ `);
584
+ await pool.query(`
585
+ CREATE TABLE IF NOT EXISTS market_context_backfill_coverage (
586
+ source text NOT NULL,
587
+ scope text NOT NULL,
588
+ interval text NOT NULL,
589
+ from_ts timestamptz NOT NULL,
590
+ to_ts timestamptz NOT NULL,
591
+ rows_count integer NOT NULL DEFAULT 0,
592
+ checked_at timestamptz NOT NULL DEFAULT now(),
593
+ PRIMARY KEY (source, scope, interval, from_ts, to_ts)
594
+ )
595
+ `);
596
+ await pool.query(`
597
+ CREATE INDEX IF NOT EXISTS market_context_backfill_coverage_lookup_idx
598
+ ON market_context_backfill_coverage (source, scope, interval, from_ts, to_ts)
599
+ `);
600
+ binanceMarketSchemaReady = true;
601
+ }
602
+ ).finally(() => {
603
+ binanceMarketSchemaReadyPromise = null;
604
+ });
605
+ await binanceMarketSchemaReadyPromise;
170
606
  };
171
607
  async function upsertDerivatives(rows) {
172
608
  if (!rows.length) return;
@@ -268,6 +704,85 @@ async function getDerivativesDataEdgesForSymbols(symbols, interval) {
268
704
  }
269
705
  return edges;
270
706
  }
707
+ async function getDerivativesBackfillCoverage(params) {
708
+ const normalizedSource = String(params.source || "").trim().toLowerCase();
709
+ const normalizedSymbols = [
710
+ ...new Set(
711
+ params.symbols.map(
712
+ (symbol) => String(symbol || "").trim().toUpperCase()
713
+ ).filter(Boolean)
714
+ )
715
+ ];
716
+ if (!normalizedSource || !normalizedSymbols.length) {
717
+ return [];
718
+ }
719
+ await ensureDerivativesSchema();
720
+ const pool = getPool();
721
+ const res = await pool.query(
722
+ `
723
+ SELECT
724
+ symbol,
725
+ interval,
726
+ extract(epoch from from_ts)*1000 AS from_ms,
727
+ extract(epoch from to_ts)*1000 AS to_ms,
728
+ rows_count
729
+ FROM derivatives_backfill_coverage
730
+ WHERE source = $1
731
+ AND symbol = ANY($2)
732
+ AND interval = $3
733
+ AND from_ts <= to_timestamp($5/1000.0)
734
+ AND to_ts >= to_timestamp($4/1000.0)
735
+ `,
736
+ [
737
+ normalizedSource,
738
+ normalizedSymbols,
739
+ params.interval,
740
+ params.fromMs,
741
+ params.toMs
742
+ ]
743
+ );
744
+ return res.rows.map((row) => ({
745
+ symbol: String(row.symbol).toUpperCase(),
746
+ interval: row.interval,
747
+ fromMs: Number(row.from_ms),
748
+ toMs: Number(row.to_ms),
749
+ rowsCount: Number(row.rows_count ?? 0)
750
+ }));
751
+ }
752
+ async function upsertDerivativesBackfillCoverage(rows) {
753
+ if (!rows.length) return;
754
+ await ensureDerivativesSchema();
755
+ const pool = getPool();
756
+ const cols = [
757
+ "source",
758
+ "symbol",
759
+ "interval",
760
+ "from_ts",
761
+ "to_ts",
762
+ "rows_count"
763
+ ];
764
+ const valuesSql = rows.map(
765
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
766
+ ).join(",");
767
+ const flat = rows.flatMap((row) => [
768
+ String(row.source || "").trim().toLowerCase(),
769
+ String(row.symbol || "").trim().toUpperCase(),
770
+ row.interval,
771
+ new Date(row.fromMs),
772
+ new Date(row.toMs),
773
+ Math.max(0, Math.trunc(row.rowsCount))
774
+ ]);
775
+ await pool.query(
776
+ `
777
+ INSERT INTO derivatives_backfill_coverage (${cols.join(",")})
778
+ VALUES ${valuesSql}
779
+ ON CONFLICT (source, symbol, interval, from_ts, to_ts) DO UPDATE SET
780
+ rows_count = EXCLUDED.rows_count,
781
+ checked_at = now()
782
+ `,
783
+ flat
784
+ );
785
+ }
271
786
  async function getDerivativesWindow(params) {
272
787
  const { symbol, intervals, endMs, lookbackMs } = params;
273
788
  const normalizedSymbol = String(symbol || "").trim().toUpperCase();
@@ -311,43 +826,114 @@ async function getDerivativesWindow(params) {
311
826
  }
312
827
  return rowsByInterval;
313
828
  }
314
- async function getDerivativesSummary(hours = 24, limit = 500) {
829
+ async function getDerivativesSummary(hours = 24, limit = 500, symbols) {
315
830
  await ensureDerivativesSchema();
316
831
  const pool = getPool();
317
- const cappedHours = Math.max(1, Math.min(24 * 30, hours));
318
- const cappedLimit = Math.max(50, Math.min(5e3, limit));
319
- const rowsQ = await pool.query(
320
- `
321
- SELECT symbol, interval, ts, open_interest, funding_rate, liq_long, liq_short, liq_total
322
- FROM derivatives_market
323
- WHERE ts >= now() - ($1 || ' hours')::interval
324
- ORDER BY ts DESC
325
- LIMIT $2
326
- `,
327
- [String(cappedHours), cappedLimit]
328
- );
329
- const aggQ = await pool.query(
832
+ const cappedHours = Math.max(1, Math.min(24 * 90, hours));
833
+ const cappedLimit = Math.max(10, Math.min(1e3, limit));
834
+ const normalizedSymbols = Array.isArray(symbols) ? [...new Set(symbols.map(normalizeCandleSymbol).filter(Boolean))] : [];
835
+ const symbolsFilterSql = normalizedSymbols.length ? "AND symbol = ANY($3)" : "";
836
+ const summaryQ = await pool.query(
330
837
  `
838
+ WITH filtered AS (
839
+ SELECT
840
+ symbol,
841
+ interval,
842
+ ts,
843
+ open_interest,
844
+ funding_rate,
845
+ liq_long,
846
+ liq_short,
847
+ liq_total
848
+ FROM derivatives_market
849
+ WHERE ts >= now() - ($1 || ' hours')::interval
850
+ ${symbolsFilterSql}
851
+ ),
852
+ latest AS (
853
+ SELECT DISTINCT ON (symbol, interval)
854
+ symbol,
855
+ interval,
856
+ ts AS last_ts,
857
+ open_interest AS latest_open_interest,
858
+ funding_rate AS latest_funding_rate
859
+ FROM filtered
860
+ ORDER BY symbol ASC, interval ASC, ts DESC
861
+ ),
862
+ first AS (
863
+ SELECT DISTINCT ON (symbol, interval)
864
+ symbol,
865
+ interval,
866
+ ts AS first_ts,
867
+ open_interest AS first_open_interest,
868
+ funding_rate AS first_funding_rate
869
+ FROM filtered
870
+ ORDER BY symbol ASC, interval ASC, ts ASC
871
+ ),
872
+ aggregated AS (
873
+ SELECT
874
+ symbol,
875
+ interval,
876
+ COUNT(*)::int AS points,
877
+ SUM(COALESCE(liq_long, 0)) AS sum_liq_long,
878
+ SUM(COALESCE(liq_short, 0)) AS sum_liq_short,
879
+ SUM(COALESCE(liq_total, 0)) AS sum_liq_total
880
+ FROM filtered
881
+ GROUP BY symbol, interval
882
+ )
331
883
  SELECT
332
- symbol,
333
- interval,
334
- COUNT(*)::int AS points,
335
- MAX(ts) AS last_ts,
336
- AVG(open_interest) AS avg_open_interest,
337
- AVG(funding_rate) AS avg_funding_rate,
338
- SUM(COALESCE(liq_total, 0)) AS sum_liq_total
339
- FROM derivatives_market
340
- WHERE ts >= now() - ($1 || ' hours')::interval
341
- GROUP BY symbol, interval
342
- ORDER BY points DESC, symbol ASC
343
- LIMIT 500
884
+ aggregated.symbol,
885
+ aggregated.interval,
886
+ aggregated.points,
887
+ latest.last_ts,
888
+ first.first_ts,
889
+ latest.latest_open_interest,
890
+ first.first_open_interest,
891
+ latest.latest_funding_rate,
892
+ first.first_funding_rate,
893
+ aggregated.sum_liq_long,
894
+ aggregated.sum_liq_short,
895
+ aggregated.sum_liq_total
896
+ FROM aggregated
897
+ JOIN latest
898
+ ON latest.symbol = aggregated.symbol
899
+ AND latest.interval = aggregated.interval
900
+ JOIN first
901
+ ON first.symbol = aggregated.symbol
902
+ AND first.interval = aggregated.interval
903
+ ORDER BY aggregated.sum_liq_total DESC, aggregated.symbol ASC
904
+ LIMIT $2
344
905
  `,
345
- [String(cappedHours)]
906
+ normalizedSymbols.length ? [String(cappedHours), cappedLimit, normalizedSymbols] : [String(cappedHours), cappedLimit]
346
907
  );
908
+ const items = summaryQ.rows.map((row) => {
909
+ const latestOpenInterest = row.latest_open_interest == null ? null : Number(row.latest_open_interest);
910
+ const firstOpenInterest = row.first_open_interest == null ? null : Number(row.first_open_interest);
911
+ const latestFundingRate = row.latest_funding_rate == null ? null : Number(row.latest_funding_rate);
912
+ const firstFundingRate = row.first_funding_rate == null ? null : Number(row.first_funding_rate);
913
+ const oiChange = latestOpenInterest != null && firstOpenInterest != null ? latestOpenInterest - firstOpenInterest : null;
914
+ const oiChangePct = oiChange != null && firstOpenInterest != null && Number.isFinite(firstOpenInterest) && Math.abs(firstOpenInterest) > 0 ? oiChange / Math.abs(firstOpenInterest) * 100 : null;
915
+ const fundingChange = latestFundingRate != null && firstFundingRate != null ? latestFundingRate - firstFundingRate : null;
916
+ return {
917
+ symbol: row.symbol,
918
+ interval: row.interval,
919
+ points: Number(row.points || 0),
920
+ last_ts: row.last_ts,
921
+ first_ts: row.first_ts,
922
+ latest_open_interest: latestOpenInterest,
923
+ first_open_interest: firstOpenInterest,
924
+ oi_change: oiChange,
925
+ oi_change_pct: oiChangePct,
926
+ latest_funding_rate: latestFundingRate,
927
+ first_funding_rate: firstFundingRate,
928
+ funding_change: fundingChange,
929
+ sum_liq_long: row.sum_liq_long == null ? null : Number(row.sum_liq_long),
930
+ sum_liq_short: row.sum_liq_short == null ? null : Number(row.sum_liq_short),
931
+ sum_liq_total: row.sum_liq_total == null ? null : Number(row.sum_liq_total)
932
+ };
933
+ });
347
934
  return {
348
- rows: rowsQ.rows,
349
- aggregates: aggQ.rows,
350
- hours: cappedHours
935
+ hours: cappedHours,
936
+ items
351
937
  };
352
938
  }
353
939
  async function upsertSpreadRows(rows) {
@@ -394,6 +980,1234 @@ async function upsertSpreadRows(rows) {
394
980
  `;
395
981
  await pool.query(sql, flat);
396
982
  }
983
+ async function upsertMarketTradeFlowRows(rows) {
984
+ if (!rows.length) return;
985
+ await ensureBinanceMarketSchema();
986
+ const pool = getPool();
987
+ const cols = [
988
+ "symbol",
989
+ "interval",
990
+ "ts",
991
+ "trades",
992
+ "buy_base_volume",
993
+ "sell_base_volume",
994
+ "buy_quote_volume",
995
+ "sell_quote_volume",
996
+ "net_base_delta",
997
+ "net_quote_delta",
998
+ "buy_pressure_pct",
999
+ "source"
1000
+ ];
1001
+ const maxRows = getSafeBulkInsertRows(cols.length);
1002
+ if (rows.length > maxRows) {
1003
+ for (let i = 0; i < rows.length; i += maxRows) {
1004
+ await upsertMarketTradeFlowRows(rows.slice(i, i + maxRows));
1005
+ }
1006
+ return;
1007
+ }
1008
+ const valuesSql = rows.map(
1009
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1010
+ ).join(",");
1011
+ const flat = rows.flatMap((row) => [
1012
+ row.symbol,
1013
+ row.interval,
1014
+ row.ts,
1015
+ row.trades,
1016
+ row.buyBaseVolume ?? null,
1017
+ row.sellBaseVolume ?? null,
1018
+ row.buyQuoteVolume ?? null,
1019
+ row.sellQuoteVolume ?? null,
1020
+ row.netBaseDelta ?? null,
1021
+ row.netQuoteDelta ?? null,
1022
+ row.buyPressurePct ?? null,
1023
+ row.source ?? null
1024
+ ]);
1025
+ await pool.query(
1026
+ `
1027
+ INSERT INTO market_trade_flow (${cols.join(",")})
1028
+ VALUES ${valuesSql}
1029
+ ON CONFLICT (symbol, interval, ts) DO UPDATE SET
1030
+ trades = EXCLUDED.trades,
1031
+ buy_base_volume = COALESCE(EXCLUDED.buy_base_volume, market_trade_flow.buy_base_volume),
1032
+ sell_base_volume = COALESCE(EXCLUDED.sell_base_volume, market_trade_flow.sell_base_volume),
1033
+ buy_quote_volume = COALESCE(EXCLUDED.buy_quote_volume, market_trade_flow.buy_quote_volume),
1034
+ sell_quote_volume = COALESCE(EXCLUDED.sell_quote_volume, market_trade_flow.sell_quote_volume),
1035
+ net_base_delta = COALESCE(EXCLUDED.net_base_delta, market_trade_flow.net_base_delta),
1036
+ net_quote_delta = COALESCE(EXCLUDED.net_quote_delta, market_trade_flow.net_quote_delta),
1037
+ buy_pressure_pct = COALESCE(EXCLUDED.buy_pressure_pct, market_trade_flow.buy_pressure_pct),
1038
+ source = COALESCE(EXCLUDED.source, market_trade_flow.source),
1039
+ ingested_at = now()
1040
+ `,
1041
+ flat
1042
+ );
1043
+ }
1044
+ async function upsertMarketBreadthRows(rows) {
1045
+ if (!rows.length) return;
1046
+ await ensureBinanceMarketSchema();
1047
+ const pool = getPool();
1048
+ const cols = [
1049
+ "universe",
1050
+ "interval",
1051
+ "ts",
1052
+ "symbols_count",
1053
+ "advancers",
1054
+ "decliners",
1055
+ "unchanged",
1056
+ "advance_decline_ratio",
1057
+ "pct_above_ma20",
1058
+ "pct_above_ma50",
1059
+ "equal_weighted_return",
1060
+ "volume_weighted_return",
1061
+ "dispersion",
1062
+ "btc_return_1h",
1063
+ "btc_return_4h",
1064
+ "btc_return_24h",
1065
+ "alt_basket_return_1h",
1066
+ "alt_basket_return_4h",
1067
+ "alt_basket_return_24h",
1068
+ "btc_vs_alt_return_1h",
1069
+ "btc_vs_alt_return_4h",
1070
+ "btc_vs_alt_return_24h",
1071
+ "btc_turnover_share_1h",
1072
+ "btc_turnover_share_24h",
1073
+ "btc_turnover_share_change_24h",
1074
+ "alt_vol_to_btc_vol_24h",
1075
+ "alt_dispersion_24h",
1076
+ "btc_alt_regime",
1077
+ "source"
1078
+ ];
1079
+ const maxRows = getSafeBulkInsertRows(cols.length);
1080
+ if (rows.length > maxRows) {
1081
+ for (let i = 0; i < rows.length; i += maxRows) {
1082
+ await upsertMarketBreadthRows(rows.slice(i, i + maxRows));
1083
+ }
1084
+ return;
1085
+ }
1086
+ const valuesSql = rows.map(
1087
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1088
+ ).join(",");
1089
+ const flat = rows.flatMap((row) => [
1090
+ row.universe,
1091
+ row.interval,
1092
+ row.ts,
1093
+ row.symbolsCount,
1094
+ row.advancers,
1095
+ row.decliners,
1096
+ row.unchanged,
1097
+ row.advanceDeclineRatio ?? null,
1098
+ row.pctAboveMa20 ?? null,
1099
+ row.pctAboveMa50 ?? null,
1100
+ row.equalWeightedReturn ?? null,
1101
+ row.volumeWeightedReturn ?? null,
1102
+ row.dispersion ?? null,
1103
+ row.btcReturn1h ?? null,
1104
+ row.btcReturn4h ?? null,
1105
+ row.btcReturn24h ?? null,
1106
+ row.altBasketReturn1h ?? null,
1107
+ row.altBasketReturn4h ?? null,
1108
+ row.altBasketReturn24h ?? null,
1109
+ row.btcVsAltReturn1h ?? null,
1110
+ row.btcVsAltReturn4h ?? null,
1111
+ row.btcVsAltReturn24h ?? null,
1112
+ row.btcTurnoverShare1h ?? null,
1113
+ row.btcTurnoverShare24h ?? null,
1114
+ row.btcTurnoverShareChange24h ?? null,
1115
+ row.altVolToBtcVol24h ?? null,
1116
+ row.altDispersion24h ?? null,
1117
+ row.btcAltRegime ?? null,
1118
+ row.source ?? null
1119
+ ]);
1120
+ await pool.query(
1121
+ `
1122
+ INSERT INTO market_breadth (${cols.join(",")})
1123
+ VALUES ${valuesSql}
1124
+ ON CONFLICT (universe, interval, ts) DO UPDATE SET
1125
+ symbols_count = EXCLUDED.symbols_count,
1126
+ advancers = EXCLUDED.advancers,
1127
+ decliners = EXCLUDED.decliners,
1128
+ unchanged = EXCLUDED.unchanged,
1129
+ advance_decline_ratio = COALESCE(EXCLUDED.advance_decline_ratio, market_breadth.advance_decline_ratio),
1130
+ pct_above_ma20 = COALESCE(EXCLUDED.pct_above_ma20, market_breadth.pct_above_ma20),
1131
+ pct_above_ma50 = COALESCE(EXCLUDED.pct_above_ma50, market_breadth.pct_above_ma50),
1132
+ equal_weighted_return = COALESCE(EXCLUDED.equal_weighted_return, market_breadth.equal_weighted_return),
1133
+ volume_weighted_return = COALESCE(EXCLUDED.volume_weighted_return, market_breadth.volume_weighted_return),
1134
+ dispersion = COALESCE(EXCLUDED.dispersion, market_breadth.dispersion),
1135
+ btc_return_1h = COALESCE(EXCLUDED.btc_return_1h, market_breadth.btc_return_1h),
1136
+ btc_return_4h = COALESCE(EXCLUDED.btc_return_4h, market_breadth.btc_return_4h),
1137
+ btc_return_24h = COALESCE(EXCLUDED.btc_return_24h, market_breadth.btc_return_24h),
1138
+ alt_basket_return_1h = COALESCE(EXCLUDED.alt_basket_return_1h, market_breadth.alt_basket_return_1h),
1139
+ alt_basket_return_4h = COALESCE(EXCLUDED.alt_basket_return_4h, market_breadth.alt_basket_return_4h),
1140
+ alt_basket_return_24h = COALESCE(EXCLUDED.alt_basket_return_24h, market_breadth.alt_basket_return_24h),
1141
+ btc_vs_alt_return_1h = COALESCE(EXCLUDED.btc_vs_alt_return_1h, market_breadth.btc_vs_alt_return_1h),
1142
+ btc_vs_alt_return_4h = COALESCE(EXCLUDED.btc_vs_alt_return_4h, market_breadth.btc_vs_alt_return_4h),
1143
+ btc_vs_alt_return_24h = COALESCE(EXCLUDED.btc_vs_alt_return_24h, market_breadth.btc_vs_alt_return_24h),
1144
+ btc_turnover_share_1h = COALESCE(EXCLUDED.btc_turnover_share_1h, market_breadth.btc_turnover_share_1h),
1145
+ btc_turnover_share_24h = COALESCE(EXCLUDED.btc_turnover_share_24h, market_breadth.btc_turnover_share_24h),
1146
+ btc_turnover_share_change_24h = COALESCE(EXCLUDED.btc_turnover_share_change_24h, market_breadth.btc_turnover_share_change_24h),
1147
+ alt_vol_to_btc_vol_24h = COALESCE(EXCLUDED.alt_vol_to_btc_vol_24h, market_breadth.alt_vol_to_btc_vol_24h),
1148
+ alt_dispersion_24h = COALESCE(EXCLUDED.alt_dispersion_24h, market_breadth.alt_dispersion_24h),
1149
+ btc_alt_regime = COALESCE(EXCLUDED.btc_alt_regime, market_breadth.btc_alt_regime),
1150
+ source = COALESCE(EXCLUDED.source, market_breadth.source),
1151
+ ingested_at = now()
1152
+ `,
1153
+ flat
1154
+ );
1155
+ }
1156
+ async function upsertMarketGlobalContextRows(rows) {
1157
+ if (!rows.length) return;
1158
+ await ensureBinanceMarketSchema();
1159
+ const pool = getPool();
1160
+ const cols = [
1161
+ "source",
1162
+ "ts",
1163
+ "updated_at_ts",
1164
+ "active_cryptocurrencies",
1165
+ "active_exchanges",
1166
+ "active_market_pairs",
1167
+ "markets",
1168
+ "total_market_cap_usd",
1169
+ "total_volume_usd",
1170
+ "total_volume_reported_usd",
1171
+ "btc_dominance_pct",
1172
+ "eth_dominance_pct",
1173
+ "alt_market_cap_usd",
1174
+ "alt_volume_usd",
1175
+ "alt_volume_reported_usd",
1176
+ "btc_to_alt_market_cap_ratio",
1177
+ "market_cap_change_pct_24h_usd"
1178
+ ];
1179
+ const maxRows = getSafeBulkInsertRows(cols.length);
1180
+ if (rows.length > maxRows) {
1181
+ for (let i = 0; i < rows.length; i += maxRows) {
1182
+ await upsertMarketGlobalContextRows(rows.slice(i, i + maxRows));
1183
+ }
1184
+ return;
1185
+ }
1186
+ const valuesSql = rows.map(
1187
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1188
+ ).join(",");
1189
+ const flat = rows.flatMap((row) => [
1190
+ row.source,
1191
+ row.ts,
1192
+ row.updatedAt ?? null,
1193
+ row.activeCryptocurrencies ?? null,
1194
+ row.activeExchanges ?? null,
1195
+ row.activeMarketPairs ?? null,
1196
+ row.markets ?? null,
1197
+ row.totalMarketCapUsd ?? null,
1198
+ row.totalVolumeUsd ?? null,
1199
+ row.totalVolumeReportedUsd ?? null,
1200
+ row.btcDominancePct ?? null,
1201
+ row.ethDominancePct ?? null,
1202
+ row.altMarketCapUsd ?? null,
1203
+ row.altVolumeUsd ?? null,
1204
+ row.altVolumeReportedUsd ?? null,
1205
+ row.btcToAltMarketCapRatio ?? null,
1206
+ row.marketCapChangePct24hUsd ?? null
1207
+ ]);
1208
+ await pool.query(
1209
+ `
1210
+ INSERT INTO market_global_context (${cols.join(",")})
1211
+ VALUES ${valuesSql}
1212
+ ON CONFLICT (source, ts) DO UPDATE SET
1213
+ updated_at_ts = COALESCE(EXCLUDED.updated_at_ts, market_global_context.updated_at_ts),
1214
+ active_cryptocurrencies = COALESCE(EXCLUDED.active_cryptocurrencies, market_global_context.active_cryptocurrencies),
1215
+ active_exchanges = COALESCE(EXCLUDED.active_exchanges, market_global_context.active_exchanges),
1216
+ active_market_pairs = COALESCE(EXCLUDED.active_market_pairs, market_global_context.active_market_pairs),
1217
+ markets = COALESCE(EXCLUDED.markets, market_global_context.markets),
1218
+ total_market_cap_usd = COALESCE(EXCLUDED.total_market_cap_usd, market_global_context.total_market_cap_usd),
1219
+ total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_global_context.total_volume_usd),
1220
+ total_volume_reported_usd = COALESCE(EXCLUDED.total_volume_reported_usd, market_global_context.total_volume_reported_usd),
1221
+ btc_dominance_pct = COALESCE(EXCLUDED.btc_dominance_pct, market_global_context.btc_dominance_pct),
1222
+ eth_dominance_pct = COALESCE(EXCLUDED.eth_dominance_pct, market_global_context.eth_dominance_pct),
1223
+ alt_market_cap_usd = COALESCE(EXCLUDED.alt_market_cap_usd, market_global_context.alt_market_cap_usd),
1224
+ alt_volume_usd = COALESCE(EXCLUDED.alt_volume_usd, market_global_context.alt_volume_usd),
1225
+ alt_volume_reported_usd = COALESCE(EXCLUDED.alt_volume_reported_usd, market_global_context.alt_volume_reported_usd),
1226
+ btc_to_alt_market_cap_ratio = COALESCE(EXCLUDED.btc_to_alt_market_cap_ratio, market_global_context.btc_to_alt_market_cap_ratio),
1227
+ market_cap_change_pct_24h_usd = COALESCE(EXCLUDED.market_cap_change_pct_24h_usd, market_global_context.market_cap_change_pct_24h_usd),
1228
+ ingested_at = now()
1229
+ `,
1230
+ flat
1231
+ );
1232
+ }
1233
+ async function upsertMarketReferenceAssetContextRows(rows) {
1234
+ if (!rows.length) return;
1235
+ await ensureBinanceMarketSchema();
1236
+ const pool = getPool();
1237
+ const cols = [
1238
+ "source",
1239
+ "symbol",
1240
+ "cmc_id",
1241
+ "interval",
1242
+ "ts",
1243
+ "open_usd",
1244
+ "high_usd",
1245
+ "low_usd",
1246
+ "close_usd",
1247
+ "volume_usd",
1248
+ "market_cap_usd"
1249
+ ];
1250
+ const maxRows = getSafeBulkInsertRows(cols.length);
1251
+ if (rows.length > maxRows) {
1252
+ for (let i = 0; i < rows.length; i += maxRows) {
1253
+ await upsertMarketReferenceAssetContextRows(rows.slice(i, i + maxRows));
1254
+ }
1255
+ return;
1256
+ }
1257
+ const valuesSql = rows.map(
1258
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1259
+ ).join(",");
1260
+ const flat = rows.flatMap((row) => [
1261
+ row.source,
1262
+ row.symbol.trim().toUpperCase(),
1263
+ Math.trunc(row.cmcId),
1264
+ row.interval,
1265
+ row.ts,
1266
+ row.openUsd ?? null,
1267
+ row.highUsd ?? null,
1268
+ row.lowUsd ?? null,
1269
+ row.closeUsd ?? null,
1270
+ row.volumeUsd ?? null,
1271
+ row.marketCapUsd ?? null
1272
+ ]);
1273
+ await pool.query(
1274
+ `
1275
+ INSERT INTO market_reference_asset_context (${cols.join(",")})
1276
+ VALUES ${valuesSql}
1277
+ ON CONFLICT (source, symbol, interval, ts) DO UPDATE SET
1278
+ cmc_id = EXCLUDED.cmc_id,
1279
+ open_usd = COALESCE(EXCLUDED.open_usd, market_reference_asset_context.open_usd),
1280
+ high_usd = COALESCE(EXCLUDED.high_usd, market_reference_asset_context.high_usd),
1281
+ low_usd = COALESCE(EXCLUDED.low_usd, market_reference_asset_context.low_usd),
1282
+ close_usd = COALESCE(EXCLUDED.close_usd, market_reference_asset_context.close_usd),
1283
+ volume_usd = COALESCE(EXCLUDED.volume_usd, market_reference_asset_context.volume_usd),
1284
+ market_cap_usd = COALESCE(EXCLUDED.market_cap_usd, market_reference_asset_context.market_cap_usd),
1285
+ ingested_at = now()
1286
+ `,
1287
+ flat
1288
+ );
1289
+ }
1290
+ async function upsertMarketCmcExchangeLiquidityContextRows(rows) {
1291
+ if (!rows.length) return;
1292
+ await ensureBinanceMarketSchema();
1293
+ const pool = getPool();
1294
+ const cols = [
1295
+ "source",
1296
+ "interval",
1297
+ "ts",
1298
+ "exchanges_count",
1299
+ "total_volume_usd",
1300
+ "binance_volume_usd",
1301
+ "binance_volume_share",
1302
+ "top_exchange_volume_share",
1303
+ "liquidity_regime"
1304
+ ];
1305
+ const maxRows = getSafeBulkInsertRows(cols.length);
1306
+ if (rows.length > maxRows) {
1307
+ for (let i = 0; i < rows.length; i += maxRows) {
1308
+ await upsertMarketCmcExchangeLiquidityContextRows(
1309
+ rows.slice(i, i + maxRows)
1310
+ );
1311
+ }
1312
+ return;
1313
+ }
1314
+ const valuesSql = rows.map(
1315
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1316
+ ).join(",");
1317
+ const flat = rows.flatMap((row) => [
1318
+ row.source,
1319
+ row.interval,
1320
+ row.ts,
1321
+ Math.trunc(row.exchangesCount),
1322
+ row.totalVolumeUsd ?? null,
1323
+ row.binanceVolumeUsd ?? null,
1324
+ row.binanceVolumeShare ?? null,
1325
+ row.topExchangeVolumeShare ?? null,
1326
+ row.liquidityRegime ?? null
1327
+ ]);
1328
+ await pool.query(
1329
+ `
1330
+ INSERT INTO market_cmc_exchange_liquidity_context (${cols.join(",")})
1331
+ VALUES ${valuesSql}
1332
+ ON CONFLICT (source, interval, ts) DO UPDATE SET
1333
+ exchanges_count = EXCLUDED.exchanges_count,
1334
+ total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_cmc_exchange_liquidity_context.total_volume_usd),
1335
+ binance_volume_usd = COALESCE(EXCLUDED.binance_volume_usd, market_cmc_exchange_liquidity_context.binance_volume_usd),
1336
+ binance_volume_share = COALESCE(EXCLUDED.binance_volume_share, market_cmc_exchange_liquidity_context.binance_volume_share),
1337
+ top_exchange_volume_share = COALESCE(EXCLUDED.top_exchange_volume_share, market_cmc_exchange_liquidity_context.top_exchange_volume_share),
1338
+ liquidity_regime = COALESCE(EXCLUDED.liquidity_regime, market_cmc_exchange_liquidity_context.liquidity_regime),
1339
+ ingested_at = now()
1340
+ `,
1341
+ flat
1342
+ );
1343
+ }
1344
+ async function upsertMarketCmcFearGreedContextRows(rows) {
1345
+ if (!rows.length) return;
1346
+ await ensureBinanceMarketSchema();
1347
+ const pool = getPool();
1348
+ const cols = [
1349
+ "source",
1350
+ "interval",
1351
+ "ts",
1352
+ "value",
1353
+ "classification",
1354
+ "sentiment_regime"
1355
+ ];
1356
+ const maxRows = getSafeBulkInsertRows(cols.length);
1357
+ if (rows.length > maxRows) {
1358
+ for (let i = 0; i < rows.length; i += maxRows) {
1359
+ await upsertMarketCmcFearGreedContextRows(rows.slice(i, i + maxRows));
1360
+ }
1361
+ return;
1362
+ }
1363
+ const valuesSql = rows.map(
1364
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1365
+ ).join(",");
1366
+ const flat = rows.flatMap((row) => [
1367
+ row.source,
1368
+ row.interval,
1369
+ row.ts,
1370
+ Math.trunc(row.value),
1371
+ row.classification,
1372
+ row.sentimentRegime
1373
+ ]);
1374
+ await pool.query(
1375
+ `
1376
+ INSERT INTO market_cmc_fear_greed_context (${cols.join(",")})
1377
+ VALUES ${valuesSql}
1378
+ ON CONFLICT (source, interval, ts) DO UPDATE SET
1379
+ value = EXCLUDED.value,
1380
+ classification = EXCLUDED.classification,
1381
+ sentiment_regime = EXCLUDED.sentiment_regime,
1382
+ ingested_at = now()
1383
+ `,
1384
+ flat
1385
+ );
1386
+ }
1387
+ async function upsertMarketCmcIndexContextRows(rows) {
1388
+ if (!rows.length) return;
1389
+ await ensureBinanceMarketSchema();
1390
+ const pool = getPool();
1391
+ const cols = [
1392
+ "source",
1393
+ "index_slug",
1394
+ "interval",
1395
+ "ts",
1396
+ "value",
1397
+ "constituents_count",
1398
+ "top_constituent_symbol",
1399
+ "top_constituent_weight_pct",
1400
+ "constituents"
1401
+ ];
1402
+ const maxRows = getSafeBulkInsertRows(cols.length);
1403
+ if (rows.length > maxRows) {
1404
+ for (let i = 0; i < rows.length; i += maxRows) {
1405
+ await upsertMarketCmcIndexContextRows(rows.slice(i, i + maxRows));
1406
+ }
1407
+ return;
1408
+ }
1409
+ const valuesSql = rows.map(
1410
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1411
+ ).join(",");
1412
+ const flat = rows.flatMap((row) => [
1413
+ row.source,
1414
+ row.indexSlug,
1415
+ row.interval,
1416
+ row.ts,
1417
+ row.value,
1418
+ row.constituentsCount ?? null,
1419
+ row.topConstituentSymbol ?? null,
1420
+ row.topConstituentWeightPct ?? null,
1421
+ row.constituents ? JSON.stringify(row.constituents) : null
1422
+ ]);
1423
+ await pool.query(
1424
+ `
1425
+ INSERT INTO market_cmc_index_context (${cols.join(",")})
1426
+ VALUES ${valuesSql}
1427
+ ON CONFLICT (source, index_slug, interval, ts) DO UPDATE SET
1428
+ value = EXCLUDED.value,
1429
+ constituents_count = COALESCE(EXCLUDED.constituents_count, market_cmc_index_context.constituents_count),
1430
+ top_constituent_symbol = COALESCE(EXCLUDED.top_constituent_symbol, market_cmc_index_context.top_constituent_symbol),
1431
+ top_constituent_weight_pct = COALESCE(EXCLUDED.top_constituent_weight_pct, market_cmc_index_context.top_constituent_weight_pct),
1432
+ constituents = COALESCE(EXCLUDED.constituents, market_cmc_index_context.constituents),
1433
+ ingested_at = now()
1434
+ `,
1435
+ flat
1436
+ );
1437
+ }
1438
+ async function getMarketContextBackfillCoverage(params) {
1439
+ const source = String(params.source || "").trim().toLowerCase();
1440
+ const scopes = [
1441
+ ...new Set(
1442
+ params.scopes.map(
1443
+ (scope) => String(scope || "").trim().toLowerCase()
1444
+ ).filter(Boolean)
1445
+ )
1446
+ ];
1447
+ const interval = String(params.interval || "").trim().toLowerCase();
1448
+ if (!source || !scopes.length || !interval) return [];
1449
+ await ensureBinanceMarketSchema();
1450
+ const pool = getPool();
1451
+ const res = await pool.query(
1452
+ `
1453
+ SELECT
1454
+ source,
1455
+ scope,
1456
+ interval,
1457
+ extract(epoch from from_ts)*1000 AS from_ms,
1458
+ extract(epoch from to_ts)*1000 AS to_ms,
1459
+ rows_count,
1460
+ extract(epoch from checked_at)*1000 AS checked_at_ms
1461
+ FROM market_context_backfill_coverage
1462
+ WHERE source = $1
1463
+ AND scope = ANY($2)
1464
+ AND interval = $3
1465
+ AND from_ts >= to_timestamp($4/1000.0)
1466
+ AND to_ts <= to_timestamp($5/1000.0)
1467
+ `,
1468
+ [source, scopes, interval, params.fromMs, params.toMs]
1469
+ );
1470
+ return res.rows.map((row) => {
1471
+ const checkedAtMs = Number(row.checked_at_ms);
1472
+ return {
1473
+ source: String(row.source).toLowerCase(),
1474
+ scope: String(row.scope).toLowerCase(),
1475
+ interval: String(row.interval).toLowerCase(),
1476
+ fromMs: Number(row.from_ms),
1477
+ toMs: Number(row.to_ms),
1478
+ rowsCount: Number(row.rows_count ?? 0),
1479
+ ...Number.isFinite(checkedAtMs) ? { checkedAtMs } : {}
1480
+ };
1481
+ });
1482
+ }
1483
+ async function upsertMarketContextBackfillCoverage(rows) {
1484
+ const normalizedRows = rows.map((row) => ({
1485
+ source: String(row.source || "").trim().toLowerCase(),
1486
+ scope: String(row.scope || "").trim().toLowerCase(),
1487
+ interval: String(row.interval || "").trim().toLowerCase(),
1488
+ fromMs: Math.trunc(row.fromMs),
1489
+ toMs: Math.trunc(row.toMs),
1490
+ rowsCount: Math.max(0, Math.trunc(row.rowsCount))
1491
+ })).filter(
1492
+ (row) => row.source && row.scope && row.interval && Number.isFinite(row.fromMs) && Number.isFinite(row.toMs) && row.toMs >= row.fromMs
1493
+ );
1494
+ if (!normalizedRows.length) return;
1495
+ await ensureBinanceMarketSchema();
1496
+ const pool = getPool();
1497
+ const cols = [
1498
+ "source",
1499
+ "scope",
1500
+ "interval",
1501
+ "from_ts",
1502
+ "to_ts",
1503
+ "rows_count"
1504
+ ];
1505
+ const valuesSql = normalizedRows.map(
1506
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1507
+ ).join(",");
1508
+ const flat = normalizedRows.flatMap((row) => [
1509
+ row.source,
1510
+ row.scope,
1511
+ row.interval,
1512
+ new Date(row.fromMs),
1513
+ new Date(row.toMs),
1514
+ row.rowsCount
1515
+ ]);
1516
+ await pool.query(
1517
+ `
1518
+ INSERT INTO market_context_backfill_coverage (${cols.join(",")})
1519
+ VALUES ${valuesSql}
1520
+ ON CONFLICT (source, scope, interval, from_ts, to_ts) DO UPDATE SET
1521
+ rows_count = EXCLUDED.rows_count,
1522
+ checked_at = now()
1523
+ `,
1524
+ flat
1525
+ );
1526
+ }
1527
+ var toMarketFeatureAge = (rowTs, atMs) => {
1528
+ const ageMs = atMs - rowTs.getTime();
1529
+ return Number.isFinite(ageMs) ? ageMs : null;
1530
+ };
1531
+ async function getLatestMarketTradeFlow(params) {
1532
+ await ensureBinanceMarketSchema();
1533
+ const pool = getPool();
1534
+ const res = await pool.query(
1535
+ `
1536
+ SELECT
1537
+ symbol,
1538
+ interval,
1539
+ ts,
1540
+ trades::int AS trades,
1541
+ buy_base_volume AS "buyBaseVolume",
1542
+ sell_base_volume AS "sellBaseVolume",
1543
+ buy_quote_volume AS "buyQuoteVolume",
1544
+ sell_quote_volume AS "sellQuoteVolume",
1545
+ net_base_delta AS "netBaseDelta",
1546
+ net_quote_delta AS "netQuoteDelta",
1547
+ buy_pressure_pct AS "buyPressurePct",
1548
+ source
1549
+ FROM market_trade_flow
1550
+ WHERE symbol = $1
1551
+ AND interval = $2
1552
+ AND ts <= to_timestamp($3/1000.0)
1553
+ ORDER BY ts DESC
1554
+ LIMIT 1
1555
+ `,
1556
+ [params.symbol.toUpperCase(), params.interval, params.atMs]
1557
+ );
1558
+ const row = res.rows[0];
1559
+ if (!row) return null;
1560
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1561
+ return {
1562
+ ...row,
1563
+ ageMs,
1564
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
1565
+ };
1566
+ }
1567
+ async function getLatestMarketBreadth(params) {
1568
+ await ensureBinanceMarketSchema();
1569
+ const pool = getPool();
1570
+ const res = await pool.query(
1571
+ `
1572
+ SELECT
1573
+ universe,
1574
+ interval,
1575
+ ts,
1576
+ symbols_count::int AS "symbolsCount",
1577
+ advancers::int AS advancers,
1578
+ decliners::int AS decliners,
1579
+ unchanged::int AS unchanged,
1580
+ advance_decline_ratio AS "advanceDeclineRatio",
1581
+ pct_above_ma20 AS "pctAboveMa20",
1582
+ pct_above_ma50 AS "pctAboveMa50",
1583
+ equal_weighted_return AS "equalWeightedReturn",
1584
+ volume_weighted_return AS "volumeWeightedReturn",
1585
+ dispersion,
1586
+ btc_return_1h AS "btcReturn1h",
1587
+ btc_return_4h AS "btcReturn4h",
1588
+ btc_return_24h AS "btcReturn24h",
1589
+ alt_basket_return_1h AS "altBasketReturn1h",
1590
+ alt_basket_return_4h AS "altBasketReturn4h",
1591
+ alt_basket_return_24h AS "altBasketReturn24h",
1592
+ btc_vs_alt_return_1h AS "btcVsAltReturn1h",
1593
+ btc_vs_alt_return_4h AS "btcVsAltReturn4h",
1594
+ btc_vs_alt_return_24h AS "btcVsAltReturn24h",
1595
+ btc_turnover_share_1h AS "btcTurnoverShare1h",
1596
+ btc_turnover_share_24h AS "btcTurnoverShare24h",
1597
+ btc_turnover_share_change_24h AS "btcTurnoverShareChange24h",
1598
+ alt_vol_to_btc_vol_24h AS "altVolToBtcVol24h",
1599
+ alt_dispersion_24h AS "altDispersion24h",
1600
+ btc_alt_regime AS "btcAltRegime",
1601
+ source
1602
+ FROM market_breadth
1603
+ WHERE universe = $1
1604
+ AND interval = $2
1605
+ AND ts <= to_timestamp($3/1000.0)
1606
+ ORDER BY ts DESC
1607
+ LIMIT 1
1608
+ `,
1609
+ [params.universe, params.interval, params.atMs]
1610
+ );
1611
+ const row = res.rows[0];
1612
+ if (!row) return null;
1613
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1614
+ return {
1615
+ ...row,
1616
+ ageMs,
1617
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
1618
+ };
1619
+ }
1620
+ async function getLatestMarketGlobalContext(params) {
1621
+ await ensureBinanceMarketSchema();
1622
+ const pool = getPool();
1623
+ const source = params.source ?? "coinmarketcap_global";
1624
+ const res = await pool.query(
1625
+ `
1626
+ SELECT
1627
+ source,
1628
+ ts,
1629
+ updated_at_ts AS "updatedAt",
1630
+ active_cryptocurrencies::int AS "activeCryptocurrencies",
1631
+ active_exchanges::int AS "activeExchanges",
1632
+ active_market_pairs::int AS "activeMarketPairs",
1633
+ markets::int AS markets,
1634
+ total_market_cap_usd AS "totalMarketCapUsd",
1635
+ total_volume_usd AS "totalVolumeUsd",
1636
+ total_volume_reported_usd AS "totalVolumeReportedUsd",
1637
+ btc_dominance_pct AS "btcDominancePct",
1638
+ eth_dominance_pct AS "ethDominancePct",
1639
+ alt_market_cap_usd AS "altMarketCapUsd",
1640
+ alt_volume_usd AS "altVolumeUsd",
1641
+ alt_volume_reported_usd AS "altVolumeReportedUsd",
1642
+ btc_to_alt_market_cap_ratio AS "btcToAltMarketCapRatio",
1643
+ market_cap_change_pct_24h_usd AS "marketCapChangePct24hUsd"
1644
+ FROM market_global_context
1645
+ WHERE source = $1
1646
+ AND ts <= to_timestamp($2/1000.0)
1647
+ ORDER BY ts DESC
1648
+ LIMIT 1
1649
+ `,
1650
+ [source, params.atMs]
1651
+ );
1652
+ const row = res.rows[0];
1653
+ if (!row) return null;
1654
+ const previousRes = await pool.query(
1655
+ `
1656
+ SELECT
1657
+ btc_dominance_pct AS "btcDominancePct",
1658
+ eth_dominance_pct AS "ethDominancePct",
1659
+ alt_market_cap_usd AS "altMarketCapUsd",
1660
+ alt_volume_usd AS "altVolumeUsd"
1661
+ FROM market_global_context
1662
+ WHERE source = $1
1663
+ AND ts <= $2::timestamptz - interval '24 hours'
1664
+ ORDER BY ts DESC
1665
+ LIMIT 1
1666
+ `,
1667
+ [source, row.ts]
1668
+ );
1669
+ const previousDominance = previousRes.rows[0]?.btcDominancePct == null ? null : Number(previousRes.rows[0].btcDominancePct);
1670
+ const previousEthDominance = previousRes.rows[0]?.ethDominancePct == null ? null : Number(previousRes.rows[0].ethDominancePct);
1671
+ const previousAltMarketCap = previousRes.rows[0]?.altMarketCapUsd == null ? null : Number(previousRes.rows[0].altMarketCapUsd);
1672
+ const previousAltVolume = previousRes.rows[0]?.altVolumeUsd == null ? null : Number(previousRes.rows[0].altVolumeUsd);
1673
+ const currentDominance = row.btcDominancePct == null ? null : Number(row.btcDominancePct);
1674
+ const currentEthDominance = row.ethDominancePct == null ? null : Number(row.ethDominancePct);
1675
+ const currentAltMarketCap = row.altMarketCapUsd == null ? null : Number(row.altMarketCapUsd);
1676
+ const currentAltVolume = row.altVolumeUsd == null ? null : Number(row.altVolumeUsd);
1677
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1678
+ return {
1679
+ ...row,
1680
+ ageMs,
1681
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1682
+ btcDominanceChange24hPct: currentDominance != null && previousDominance != null ? currentDominance - previousDominance : null,
1683
+ ethDominanceChange24hPct: currentEthDominance != null && previousEthDominance != null ? currentEthDominance - previousEthDominance : null,
1684
+ altMarketCapChange24hPct: currentAltMarketCap != null && previousAltMarketCap != null && previousAltMarketCap > 0 ? (currentAltMarketCap - previousAltMarketCap) / previousAltMarketCap : null,
1685
+ altVolumeChange24hPct: currentAltVolume != null && previousAltVolume != null && previousAltVolume > 0 ? (currentAltVolume - previousAltVolume) / previousAltVolume : null
1686
+ };
1687
+ }
1688
+ async function getMarketGlobalContextCoverage(params) {
1689
+ await ensureBinanceMarketSchema();
1690
+ const pool = getPool();
1691
+ const res = await pool.query(
1692
+ `
1693
+ SELECT
1694
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1695
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1696
+ COUNT(*)::int AS rows
1697
+ FROM market_global_context
1698
+ WHERE source = $1
1699
+ AND ts >= to_timestamp($2/1000.0)
1700
+ AND ts <= to_timestamp($3/1000.0)
1701
+ `,
1702
+ [params.source, params.startMs, params.endMs]
1703
+ );
1704
+ const row = res.rows[0];
1705
+ const rows = Number(row?.rows ?? 0);
1706
+ const firstMs = Number(row?.first_ms);
1707
+ const lastMs = Number(row?.last_ms);
1708
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
1709
+ return null;
1710
+ }
1711
+ return { firstMs, lastMs, rows };
1712
+ }
1713
+ async function getMarketReferenceAssetContextCoverage(params) {
1714
+ const symbols = [
1715
+ ...new Set(
1716
+ params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
1717
+ )
1718
+ ];
1719
+ const coverage = /* @__PURE__ */ new Map();
1720
+ if (!symbols.length) return coverage;
1721
+ await ensureBinanceMarketSchema();
1722
+ const pool = getPool();
1723
+ const res = await pool.query(
1724
+ `
1725
+ SELECT
1726
+ symbol,
1727
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1728
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1729
+ COUNT(*)::int AS rows
1730
+ FROM market_reference_asset_context
1731
+ WHERE source = $1
1732
+ AND symbol = ANY($2)
1733
+ AND interval = $3
1734
+ AND ts >= to_timestamp($4/1000.0)
1735
+ AND ts <= to_timestamp($5/1000.0)
1736
+ GROUP BY symbol
1737
+ `,
1738
+ [params.source, symbols, params.interval, params.startMs, params.endMs]
1739
+ );
1740
+ for (const row of res.rows) {
1741
+ const firstMs = Number(row.first_ms);
1742
+ const lastMs = Number(row.last_ms);
1743
+ const rows = Number(row.rows);
1744
+ if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
1745
+ coverage.set(row.symbol.toUpperCase(), { firstMs, lastMs, rows });
1746
+ }
1747
+ }
1748
+ return coverage;
1749
+ }
1750
+ async function getLatestMarketReferenceAssetContexts(params) {
1751
+ const source = params.source ?? "coinmarketcap_reference_asset";
1752
+ const interval = params.interval ?? "1d";
1753
+ const symbols = [
1754
+ ...new Set(
1755
+ params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
1756
+ )
1757
+ ];
1758
+ const rows = /* @__PURE__ */ new Map();
1759
+ if (!symbols.length) return rows;
1760
+ await ensureBinanceMarketSchema();
1761
+ const pool = getPool();
1762
+ const res = await pool.query(
1763
+ `
1764
+ SELECT DISTINCT ON (symbol)
1765
+ source,
1766
+ symbol,
1767
+ cmc_id AS "cmcId",
1768
+ interval,
1769
+ ts,
1770
+ open_usd AS "openUsd",
1771
+ high_usd AS "highUsd",
1772
+ low_usd AS "lowUsd",
1773
+ close_usd AS "closeUsd",
1774
+ volume_usd AS "volumeUsd",
1775
+ market_cap_usd AS "marketCapUsd"
1776
+ FROM market_reference_asset_context
1777
+ WHERE source = $1
1778
+ AND symbol = ANY($2)
1779
+ AND interval = $3
1780
+ AND ts <= to_timestamp($4/1000.0)
1781
+ ORDER BY symbol ASC, ts DESC
1782
+ `,
1783
+ [source, symbols, interval, params.atMs]
1784
+ );
1785
+ for (const row of res.rows) {
1786
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1787
+ rows.set(row.symbol.toUpperCase(), {
1788
+ ...row,
1789
+ ageMs,
1790
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
1791
+ });
1792
+ }
1793
+ return rows;
1794
+ }
1795
+ async function getLatestMarketCmcExchangeLiquidityContext(params) {
1796
+ await ensureBinanceMarketSchema();
1797
+ const pool = getPool();
1798
+ const source = params.source ?? "coinmarketcap_exchange_liquidity";
1799
+ const interval = params.interval ?? "1d";
1800
+ const res = await pool.query(
1801
+ `
1802
+ SELECT
1803
+ source,
1804
+ interval,
1805
+ ts,
1806
+ exchanges_count::int AS "exchangesCount",
1807
+ total_volume_usd AS "totalVolumeUsd",
1808
+ binance_volume_usd AS "binanceVolumeUsd",
1809
+ binance_volume_share AS "binanceVolumeShare",
1810
+ top_exchange_volume_share AS "topExchangeVolumeShare",
1811
+ liquidity_regime AS "liquidityRegime"
1812
+ FROM market_cmc_exchange_liquidity_context
1813
+ WHERE source = $1
1814
+ AND interval = $2
1815
+ AND ts <= to_timestamp($3/1000.0)
1816
+ ORDER BY ts DESC
1817
+ LIMIT 1
1818
+ `,
1819
+ [source, interval, params.atMs]
1820
+ );
1821
+ const row = res.rows[0];
1822
+ if (!row) return null;
1823
+ const previousRes = await pool.query(
1824
+ `
1825
+ SELECT total_volume_usd AS "totalVolumeUsd"
1826
+ FROM market_cmc_exchange_liquidity_context
1827
+ WHERE source = $1
1828
+ AND interval = $2
1829
+ AND ts <= $3::timestamptz - interval '24 hours'
1830
+ ORDER BY ts DESC
1831
+ LIMIT 1
1832
+ `,
1833
+ [source, interval, row.ts]
1834
+ );
1835
+ const currentTotal = row.totalVolumeUsd == null ? null : Number(row.totalVolumeUsd);
1836
+ const previousTotal = previousRes.rows[0]?.totalVolumeUsd == null ? null : Number(previousRes.rows[0].totalVolumeUsd);
1837
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1838
+ return {
1839
+ ...row,
1840
+ ageMs,
1841
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1842
+ totalVolumeChange24hPct: currentTotal != null && previousTotal != null && previousTotal > 0 ? (currentTotal - previousTotal) / previousTotal : null
1843
+ };
1844
+ }
1845
+ async function getLatestMarketCmcIndexContexts(params) {
1846
+ const source = params.source ?? "coinmarketcap_index";
1847
+ const interval = params.interval ?? "1d";
1848
+ const indexSlugs = [
1849
+ ...new Set(
1850
+ params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
1851
+ (slug) => ["cmc100", "cmc20"].includes(slug)
1852
+ )
1853
+ )
1854
+ ];
1855
+ const rows = /* @__PURE__ */ new Map();
1856
+ if (!indexSlugs.length) return rows;
1857
+ await ensureBinanceMarketSchema();
1858
+ const pool = getPool();
1859
+ const res = await pool.query(
1860
+ `
1861
+ SELECT DISTINCT ON (index_slug)
1862
+ source,
1863
+ index_slug AS "indexSlug",
1864
+ interval,
1865
+ ts,
1866
+ value,
1867
+ constituents_count::int AS "constituentsCount",
1868
+ top_constituent_symbol AS "topConstituentSymbol",
1869
+ top_constituent_weight_pct AS "topConstituentWeightPct",
1870
+ constituents
1871
+ FROM market_cmc_index_context
1872
+ WHERE source = $1
1873
+ AND index_slug = ANY($2)
1874
+ AND interval = $3
1875
+ AND ts <= to_timestamp($4/1000.0)
1876
+ ORDER BY index_slug ASC, ts DESC
1877
+ `,
1878
+ [source, indexSlugs, interval, params.atMs]
1879
+ );
1880
+ for (const row of res.rows) {
1881
+ const previousRes = await pool.query(
1882
+ `
1883
+ SELECT value
1884
+ FROM market_cmc_index_context
1885
+ WHERE source = $1
1886
+ AND index_slug = $2
1887
+ AND interval = $3
1888
+ AND ts <= $4::timestamptz - interval '24 hours'
1889
+ ORDER BY ts DESC
1890
+ LIMIT 1
1891
+ `,
1892
+ [source, row.indexSlug, interval, row.ts]
1893
+ );
1894
+ const currentValue = row.value == null ? null : Number(row.value);
1895
+ const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
1896
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1897
+ rows.set(row.indexSlug, {
1898
+ ...row,
1899
+ ageMs,
1900
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1901
+ valueChange24hPct: currentValue != null && previousValue != null && previousValue > 0 ? (currentValue - previousValue) / previousValue : null
1902
+ });
1903
+ }
1904
+ return rows;
1905
+ }
1906
+ async function getLatestMarketCmcFearGreedContext(params) {
1907
+ await ensureBinanceMarketSchema();
1908
+ const pool = getPool();
1909
+ const source = params.source ?? "coinmarketcap_fear_greed";
1910
+ const interval = params.interval ?? "1d";
1911
+ const res = await pool.query(
1912
+ `
1913
+ SELECT
1914
+ source,
1915
+ interval,
1916
+ ts,
1917
+ value::int AS value,
1918
+ classification,
1919
+ sentiment_regime AS "sentimentRegime"
1920
+ FROM market_cmc_fear_greed_context
1921
+ WHERE source = $1
1922
+ AND interval = $2
1923
+ AND ts <= to_timestamp($3/1000.0)
1924
+ ORDER BY ts DESC
1925
+ LIMIT 1
1926
+ `,
1927
+ [source, interval, params.atMs]
1928
+ );
1929
+ const row = res.rows[0];
1930
+ if (!row) return null;
1931
+ const previousRes = await pool.query(
1932
+ `
1933
+ SELECT
1934
+ value::int AS value,
1935
+ '24h' AS bucket
1936
+ FROM market_cmc_fear_greed_context
1937
+ WHERE source = $1
1938
+ AND interval = $2
1939
+ AND ts <= $3::timestamptz - interval '24 hours'
1940
+ ORDER BY ts DESC
1941
+ LIMIT 1
1942
+ `,
1943
+ [source, interval, row.ts]
1944
+ );
1945
+ const previous7dRes = await pool.query(
1946
+ `
1947
+ SELECT value::int AS value
1948
+ FROM market_cmc_fear_greed_context
1949
+ WHERE source = $1
1950
+ AND interval = $2
1951
+ AND ts <= $3::timestamptz - interval '7 days'
1952
+ ORDER BY ts DESC
1953
+ LIMIT 1
1954
+ `,
1955
+ [source, interval, row.ts]
1956
+ );
1957
+ const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
1958
+ const previous7dValue = previous7dRes.rows[0]?.value == null ? null : Number(previous7dRes.rows[0].value);
1959
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1960
+ return {
1961
+ ...row,
1962
+ ageMs,
1963
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1964
+ valueChange24h: previousValue == null ? null : row.value - previousValue,
1965
+ valueChange7d: previous7dValue == null ? null : row.value - previous7dValue
1966
+ };
1967
+ }
1968
+ async function getMarketCmcFearGreedContextCoverage(params) {
1969
+ await ensureBinanceMarketSchema();
1970
+ const pool = getPool();
1971
+ const res = await pool.query(
1972
+ `
1973
+ SELECT
1974
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1975
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1976
+ COUNT(*)::int AS rows
1977
+ FROM market_cmc_fear_greed_context
1978
+ WHERE source = $1
1979
+ AND interval = $2
1980
+ AND ts >= to_timestamp($3/1000.0)
1981
+ AND ts <= to_timestamp($4/1000.0)
1982
+ `,
1983
+ [params.source, params.interval, params.startMs, params.endMs]
1984
+ );
1985
+ const rows = Number(res.rows[0]?.rows ?? 0);
1986
+ const firstMs = Number(res.rows[0]?.first_ms);
1987
+ const lastMs = Number(res.rows[0]?.last_ms);
1988
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
1989
+ return null;
1990
+ }
1991
+ return { firstMs, lastMs, rows };
1992
+ }
1993
+ async function getMarketCmcExchangeLiquidityContextCoverage(params) {
1994
+ await ensureBinanceMarketSchema();
1995
+ const pool = getPool();
1996
+ const res = await pool.query(
1997
+ `
1998
+ SELECT
1999
+ extract(epoch from MIN(ts))*1000 AS first_ms,
2000
+ extract(epoch from MAX(ts))*1000 AS last_ms,
2001
+ COUNT(*)::int AS rows
2002
+ FROM market_cmc_exchange_liquidity_context
2003
+ WHERE source = $1
2004
+ AND interval = $2
2005
+ AND ts >= to_timestamp($3/1000.0)
2006
+ AND ts <= to_timestamp($4/1000.0)
2007
+ `,
2008
+ [params.source, params.interval, params.startMs, params.endMs]
2009
+ );
2010
+ const rows = Number(res.rows[0]?.rows ?? 0);
2011
+ const firstMs = Number(res.rows[0]?.first_ms);
2012
+ const lastMs = Number(res.rows[0]?.last_ms);
2013
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
2014
+ return null;
2015
+ }
2016
+ return { firstMs, lastMs, rows };
2017
+ }
2018
+ async function getMarketCmcIndexContextCoverage(params) {
2019
+ const indexSlugs = [
2020
+ ...new Set(
2021
+ params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
2022
+ (slug) => ["cmc100", "cmc20"].includes(slug)
2023
+ )
2024
+ )
2025
+ ];
2026
+ const coverage = /* @__PURE__ */ new Map();
2027
+ if (!indexSlugs.length) return coverage;
2028
+ await ensureBinanceMarketSchema();
2029
+ const pool = getPool();
2030
+ const res = await pool.query(
2031
+ `
2032
+ SELECT
2033
+ index_slug,
2034
+ extract(epoch from MIN(ts))*1000 AS first_ms,
2035
+ extract(epoch from MAX(ts))*1000 AS last_ms,
2036
+ COUNT(*)::int AS rows
2037
+ FROM market_cmc_index_context
2038
+ WHERE source = $1
2039
+ AND index_slug = ANY($2)
2040
+ AND interval = $3
2041
+ AND ts >= to_timestamp($4/1000.0)
2042
+ AND ts <= to_timestamp($5/1000.0)
2043
+ GROUP BY index_slug
2044
+ `,
2045
+ [params.source, indexSlugs, params.interval, params.startMs, params.endMs]
2046
+ );
2047
+ for (const row of res.rows) {
2048
+ const indexSlug = row.index_slug;
2049
+ const firstMs = Number(row.first_ms);
2050
+ const lastMs = Number(row.last_ms);
2051
+ const rows = Number(row.rows);
2052
+ if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
2053
+ coverage.set(indexSlug, { firstMs, lastMs, rows });
2054
+ }
2055
+ }
2056
+ return coverage;
2057
+ }
2058
+ async function getMarketTradeFlowCoverage(params) {
2059
+ const symbols = [
2060
+ ...new Set(params.symbols.map((item) => item.toUpperCase()))
2061
+ ];
2062
+ if (!symbols.length) return /* @__PURE__ */ new Map();
2063
+ await ensureBinanceMarketSchema();
2064
+ const pool = getPool();
2065
+ const res = await pool.query(
2066
+ `
2067
+ SELECT
2068
+ symbol,
2069
+ MIN(ts) AS first_ts,
2070
+ MAX(ts) AS last_ts,
2071
+ COUNT(*)::int AS rows
2072
+ FROM market_trade_flow
2073
+ WHERE symbol = ANY($1)
2074
+ AND interval = $2
2075
+ AND ts >= to_timestamp($3/1000.0)
2076
+ AND ts <= to_timestamp($4/1000.0)
2077
+ GROUP BY symbol
2078
+ `,
2079
+ [symbols, params.interval, params.startMs, params.endMs]
2080
+ );
2081
+ return new Map(
2082
+ res.rows.map((row) => [
2083
+ String(row.symbol).toUpperCase(),
2084
+ {
2085
+ firstMs: new Date(row.first_ts).getTime(),
2086
+ lastMs: new Date(row.last_ts).getTime(),
2087
+ rows: Number(row.rows) || 0
2088
+ }
2089
+ ])
2090
+ );
2091
+ }
2092
+ var getTableRowCountIfExists = async (tableName) => {
2093
+ const pool = getPool();
2094
+ const exists = await pool.query("SELECT to_regclass($1) AS name", [
2095
+ tableName
2096
+ ]);
2097
+ if (!exists.rows[0]?.name) return null;
2098
+ const count = await pool.query(
2099
+ `SELECT COUNT(*)::int AS rows FROM ${tableName}`
2100
+ );
2101
+ return Number(count.rows[0]?.rows ?? 0);
2102
+ };
2103
+ async function cleanupDeprecatedMarketContext(params = {}) {
2104
+ const apply = Boolean(params.apply);
2105
+ const pool = getPool();
2106
+ const items = [];
2107
+ const cleanupRows = async ({
2108
+ tableName,
2109
+ whereSql,
2110
+ name
2111
+ }) => {
2112
+ const tableRows = await getTableRowCountIfExists(tableName);
2113
+ if (tableRows == null) return;
2114
+ const count = await pool.query(
2115
+ `
2116
+ SELECT COUNT(*)::int AS rows
2117
+ FROM ${tableName}
2118
+ WHERE ${whereSql}
2119
+ `
2120
+ );
2121
+ const rows = Number(count.rows[0]?.rows ?? 0);
2122
+ if (rows <= 0) return;
2123
+ if (apply) {
2124
+ await pool.query(
2125
+ `
2126
+ DELETE FROM ${tableName}
2127
+ WHERE ${whereSql}
2128
+ `
2129
+ );
2130
+ }
2131
+ items.push({
2132
+ kind: "rows",
2133
+ name,
2134
+ rows,
2135
+ action: "delete_rows",
2136
+ applied: apply
2137
+ });
2138
+ };
2139
+ for (const tableName of ["market_order_book_depth", "onchain_flow_context"]) {
2140
+ const rows = await getTableRowCountIfExists(tableName);
2141
+ if (rows == null) continue;
2142
+ if (apply) {
2143
+ await pool.query(`DROP TABLE IF EXISTS ${tableName}`);
2144
+ }
2145
+ items.push({
2146
+ kind: "table",
2147
+ name: tableName,
2148
+ rows,
2149
+ action: "drop_table",
2150
+ applied: apply
2151
+ });
2152
+ }
2153
+ await cleanupRows({
2154
+ tableName: "market_global_context",
2155
+ whereSql: "source = 'coingecko_global'",
2156
+ name: "market_global_context/source=coingecko_global"
2157
+ });
2158
+ await cleanupRows({
2159
+ tableName: "market_global_context",
2160
+ whereSql: "source = 'coinmarketcap_global_hourly'",
2161
+ name: "market_global_context/source=coinmarketcap_global_hourly"
2162
+ });
2163
+ await cleanupRows({
2164
+ tableName: "market_reference_asset_context",
2165
+ whereSql: "source = 'coinmarketcap_reference_asset' AND interval = '1h'",
2166
+ name: "market_reference_asset_context/source=coinmarketcap_reference_asset/interval=1h"
2167
+ });
2168
+ await cleanupRows({
2169
+ tableName: "market_cmc_breadth_context",
2170
+ whereSql: "source = 'coinmarketcap_market_breadth'",
2171
+ name: "market_cmc_breadth_context/source=coinmarketcap_market_breadth"
2172
+ });
2173
+ await cleanupRows({
2174
+ tableName: "market_context_backfill_coverage",
2175
+ whereSql: "(source IN ('coinmarketcap_global_hourly', 'coinmarketcap_market_breadth') OR (source = 'coinmarketcap_reference_asset' AND interval = '1h'))",
2176
+ name: "market_context_backfill_coverage/deprecated_cmc_sources"
2177
+ });
2178
+ return items;
2179
+ }
2180
+ async function getMarketBreadthCoverage(params) {
2181
+ await ensureBinanceMarketSchema();
2182
+ const pool = getPool();
2183
+ const res = await pool.query(
2184
+ `
2185
+ SELECT
2186
+ MIN(ts) AS first_ts,
2187
+ MAX(ts) AS last_ts,
2188
+ COUNT(*)::int AS rows,
2189
+ COUNT(*) FILTER (
2190
+ WHERE btc_alt_regime IS NOT NULL
2191
+ AND btc_return_24h IS NOT NULL
2192
+ AND alt_basket_return_24h IS NOT NULL
2193
+ )::int AS btc_alt_metrics_rows
2194
+ FROM market_breadth
2195
+ WHERE universe = $1
2196
+ AND interval = $2
2197
+ AND ts >= to_timestamp($3/1000.0)
2198
+ AND ts <= to_timestamp($4/1000.0)
2199
+ `,
2200
+ [params.universe, params.interval, params.startMs, params.endMs]
2201
+ );
2202
+ const row = res.rows[0];
2203
+ if (!row?.first_ts || !row?.last_ts) return null;
2204
+ return {
2205
+ firstMs: new Date(row.first_ts).getTime(),
2206
+ lastMs: new Date(row.last_ts).getTime(),
2207
+ rows: Number(row.rows) || 0,
2208
+ btcAltMetricsRows: Number(row.btc_alt_metrics_rows) || 0
2209
+ };
2210
+ }
397
2211
  async function getSpreadRangeForSymbols(symbols, interval, startMs, endMs) {
398
2212
  if (!symbols.length) {
399
2213
  return [];
@@ -451,12 +2265,17 @@ async function getSpreadSummary(hours = 24, limit = 500) {
451
2265
  };
452
2266
  }
453
2267
  async function getCandlesRange(provider, symbol, interval, startMs, endMs) {
2268
+ await ensureCandlesSchema();
454
2269
  const pool = getPool();
455
2270
  const normalizedProvider = normalizeCandleProvider(provider);
456
2271
  const normalizedSymbol = normalizeCandleSymbol(symbol);
457
2272
  const sql = `
458
2273
  SELECT symbol, interval, ts,
459
- open, high, low, close, volume, turnover
2274
+ open, high, low, close, volume, turnover,
2275
+ taker_buy_base_volume AS "takerBuyBaseVolume",
2276
+ taker_buy_quote_volume AS "takerBuyQuoteVolume",
2277
+ taker_sell_base_volume AS "takerSellBaseVolume",
2278
+ taker_sell_quote_volume AS "takerSellQuoteVolume"
460
2279
  FROM candles
461
2280
  WHERE provider = $1 AND symbol = $2 AND interval = $3
462
2281
  AND ts >= to_timestamp($4/1000.0)
@@ -473,6 +2292,7 @@ async function getCandlesRange(provider, symbol, interval, startMs, endMs) {
473
2292
  return res.rows;
474
2293
  }
475
2294
  async function getDataEdges(provider, symbol, interval) {
2295
+ await ensureCandlesSchema();
476
2296
  const pool = getPool();
477
2297
  const normalizedProvider = normalizeCandleProvider(provider);
478
2298
  const normalizedSymbol = normalizeCandleSymbol(symbol);
@@ -500,6 +2320,59 @@ async function getDataEdges(provider, symbol, interval) {
500
2320
  const max = Number.isFinite(Number(maxRaw)) ? Number(maxRaw) : void 0;
501
2321
  return { min, max };
502
2322
  }
2323
+ async function getDataEdgesForSymbols(provider, symbols, interval) {
2324
+ const normalizedSymbols = [
2325
+ ...new Set(symbols.map(normalizeCandleSymbol).filter(Boolean))
2326
+ ];
2327
+ const result = /* @__PURE__ */ new Map();
2328
+ for (const symbol of normalizedSymbols) {
2329
+ result.set(symbol, {});
2330
+ }
2331
+ if (!normalizedSymbols.length) {
2332
+ return result;
2333
+ }
2334
+ await ensureCandlesSchema();
2335
+ const pool = getPool();
2336
+ const normalizedProvider = normalizeCandleProvider(provider);
2337
+ const sql = `
2338
+ WITH requested(symbol) AS (
2339
+ SELECT unnest($2::text[])
2340
+ )
2341
+ SELECT
2342
+ r.symbol,
2343
+ (
2344
+ SELECT extract(epoch from c.ts)*1000
2345
+ FROM candles c
2346
+ WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
2347
+ ORDER BY c.ts ASC
2348
+ LIMIT 1
2349
+ ) AS min_ms,
2350
+ (
2351
+ SELECT extract(epoch from c.ts)*1000
2352
+ FROM candles c
2353
+ WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
2354
+ ORDER BY c.ts DESC
2355
+ LIMIT 1
2356
+ ) AS max_ms
2357
+ FROM requested r
2358
+ `;
2359
+ const response = await pool.query(sql, [
2360
+ normalizedProvider,
2361
+ normalizedSymbols,
2362
+ interval
2363
+ ]);
2364
+ for (const row of response.rows) {
2365
+ const symbol = normalizeCandleSymbol(String(row.symbol || ""));
2366
+ if (!symbol) continue;
2367
+ const min = row.min_ms == null ? NaN : Number(row.min_ms);
2368
+ const max = row.max_ms == null ? NaN : Number(row.max_ms);
2369
+ result.set(symbol, {
2370
+ ...Number.isFinite(min) ? { min } : {},
2371
+ ...Number.isFinite(max) ? { max } : {}
2372
+ });
2373
+ }
2374
+ return result;
2375
+ }
503
2376
  async function waitForDbReady(attempts = 20, delayMs = 1e3) {
504
2377
  const pool = getPool();
505
2378
  let lastError;
@@ -562,19 +2435,47 @@ async function findContinuityGap(provider, symbol, interval) {
562
2435
  };
563
2436
  }
564
2437
  export {
2438
+ cleanupDeprecatedMarketContext,
2439
+ closeTimescalePool,
565
2440
  deleteCandles,
566
2441
  findContinuityGap,
567
2442
  getCandlesRange,
568
2443
  getDataEdges,
2444
+ getDataEdgesForSymbols,
2445
+ getDerivativesBackfillCoverage,
569
2446
  getDerivativesDataEdgesForSymbols,
570
2447
  getDerivativesRangeForSymbols,
571
2448
  getDerivativesSummary,
572
2449
  getDerivativesWindow,
2450
+ getLatestMarketBreadth,
2451
+ getLatestMarketCmcExchangeLiquidityContext,
2452
+ getLatestMarketCmcFearGreedContext,
2453
+ getLatestMarketCmcIndexContexts,
2454
+ getLatestMarketGlobalContext,
2455
+ getLatestMarketReferenceAssetContexts,
2456
+ getLatestMarketTradeFlow,
2457
+ getMarketBreadthCoverage,
2458
+ getMarketCmcExchangeLiquidityContextCoverage,
2459
+ getMarketCmcFearGreedContextCoverage,
2460
+ getMarketCmcIndexContextCoverage,
2461
+ getMarketContextBackfillCoverage,
2462
+ getMarketGlobalContextCoverage,
2463
+ getMarketReferenceAssetContextCoverage,
2464
+ getMarketTradeFlowCoverage,
573
2465
  getSpreadRangeForSymbols,
574
2466
  getSpreadSummary,
575
2467
  toRows,
576
2468
  upsertCandles,
577
2469
  upsertDerivatives,
2470
+ upsertDerivativesBackfillCoverage,
2471
+ upsertMarketBreadthRows,
2472
+ upsertMarketCmcExchangeLiquidityContextRows,
2473
+ upsertMarketCmcFearGreedContextRows,
2474
+ upsertMarketCmcIndexContextRows,
2475
+ upsertMarketContextBackfillCoverage,
2476
+ upsertMarketGlobalContextRows,
2477
+ upsertMarketReferenceAssetContextRows,
2478
+ upsertMarketTradeFlowRows,
578
2479
  upsertSpreadRows,
579
2480
  waitForDbReady
580
2481
  };