@tradejs/infra 1.0.9 → 1.0.10

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