@tradejs/infra 2.0.18 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/ai.js +3 -54
  2. package/dist/ai.mjs +1 -54
  3. package/dist/chunk-2CZREG43.mjs +112 -0
  4. package/dist/chunk-DFMKDB2R.mjs +1285 -0
  5. package/dist/chunk-I2J6YDBD.mjs +910 -0
  6. package/dist/chunk-NWXFWTWU.mjs +1114 -0
  7. package/dist/chunk-SZQB7ER5.mjs +492 -0
  8. package/dist/chunk-YVIHTUV5.mjs +286 -0
  9. package/dist/internal-2coHaaos.d.mts +26 -0
  10. package/dist/internal-2coHaaos.d.ts +26 -0
  11. package/dist/runtimeDeployments.d.mts +10 -0
  12. package/dist/runtimeDeployments.d.ts +10 -0
  13. package/dist/runtimeDeployments.js +447 -0
  14. package/dist/runtimeDeployments.mjs +81 -0
  15. package/dist/runtimeStrategyConfigs.d.mts +28 -0
  16. package/dist/runtimeStrategyConfigs.d.ts +28 -0
  17. package/dist/runtimeStrategyConfigs.js +425 -0
  18. package/dist/runtimeStrategyConfigs.mjs +89 -0
  19. package/dist/timescale/candles.d.mts +38 -0
  20. package/dist/timescale/candles.d.ts +38 -0
  21. package/dist/timescale/candles.js +408 -0
  22. package/dist/timescale/candles.mjs +21 -0
  23. package/dist/timescale/client.d.mts +4 -0
  24. package/dist/timescale/client.d.ts +4 -0
  25. package/dist/timescale/client.js +109 -0
  26. package/dist/timescale/client.mjs +12 -0
  27. package/dist/timescale/derivatives.d.mts +90 -0
  28. package/dist/timescale/derivatives.d.ts +90 -0
  29. package/dist/timescale/derivatives.js +1270 -0
  30. package/dist/timescale/derivatives.mjs +26 -0
  31. package/dist/timescale/hyperliquidWhales.d.mts +149 -0
  32. package/dist/timescale/hyperliquidWhales.d.ts +149 -0
  33. package/dist/timescale/hyperliquidWhales.js +1893 -0
  34. package/dist/timescale/hyperliquidWhales.mjs +30 -0
  35. package/dist/timescale/marketContext.d.mts +188 -0
  36. package/dist/timescale/marketContext.d.ts +188 -0
  37. package/dist/timescale/marketContext.js +2091 -0
  38. package/dist/timescale/marketContext.mjs +60 -0
  39. package/dist/timescale/spread.d.mts +11 -0
  40. package/dist/timescale/spread.d.ts +11 -0
  41. package/dist/timescale/spread.js +215 -0
  42. package/dist/timescale/spread.mjs +11 -0
  43. package/dist/timescale.d.mts +9 -476
  44. package/dist/timescale.d.ts +9 -476
  45. package/dist/timescale.js +2121 -2112
  46. package/dist/timescale.mjs +73 -4070
  47. package/dist/tradingAccounts.d.mts +2 -8
  48. package/dist/tradingAccounts.d.ts +2 -8
  49. package/dist/tradingAccounts.js +0 -71
  50. package/dist/tradingAccounts.mjs +0 -65
  51. package/dist/values-BrvcmnfM.d.mts +6 -0
  52. package/dist/values-BrvcmnfM.d.ts +6 -0
  53. package/package.json +43 -2
@@ -0,0 +1,2091 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/timescale/marketContext.ts
21
+ var marketContext_exports = {};
22
+ __export(marketContext_exports, {
23
+ cleanupDeprecatedMarketContext: () => cleanupDeprecatedMarketContext,
24
+ ensureBinanceMarketSchema: () => ensureBinanceMarketSchema,
25
+ ensureCoinMarketCapContextSchema: () => ensureCoinMarketCapContextSchema,
26
+ ensureMarketContextSchemas: () => ensureMarketContextSchemas,
27
+ getLatestMarketBreadth: () => getLatestMarketBreadth,
28
+ getLatestMarketCmcExchangeLiquidityContext: () => getLatestMarketCmcExchangeLiquidityContext,
29
+ getLatestMarketCmcFearGreedContext: () => getLatestMarketCmcFearGreedContext,
30
+ getLatestMarketCmcIndexContexts: () => getLatestMarketCmcIndexContexts,
31
+ getLatestMarketGlobalContext: () => getLatestMarketGlobalContext,
32
+ getLatestMarketReferenceAssetContexts: () => getLatestMarketReferenceAssetContexts,
33
+ getLatestMarketTradeFlow: () => getLatestMarketTradeFlow,
34
+ getMarketBreadthCoverage: () => getMarketBreadthCoverage,
35
+ getMarketCmcExchangeLiquidityContextCoverage: () => getMarketCmcExchangeLiquidityContextCoverage,
36
+ getMarketCmcFearGreedContextCoverage: () => getMarketCmcFearGreedContextCoverage,
37
+ getMarketCmcIndexContextCoverage: () => getMarketCmcIndexContextCoverage,
38
+ getMarketContextBackfillCoverage: () => getMarketContextBackfillCoverage,
39
+ getMarketGlobalContextCoverage: () => getMarketGlobalContextCoverage,
40
+ getMarketReferenceAssetContextCoverage: () => getMarketReferenceAssetContextCoverage,
41
+ getMarketTradeFlowCoverage: () => getMarketTradeFlowCoverage,
42
+ upsertMarketBreadthRows: () => upsertMarketBreadthRows,
43
+ upsertMarketCmcExchangeLiquidityContextRows: () => upsertMarketCmcExchangeLiquidityContextRows,
44
+ upsertMarketCmcFearGreedContextRows: () => upsertMarketCmcFearGreedContextRows,
45
+ upsertMarketCmcIndexContextRows: () => upsertMarketCmcIndexContextRows,
46
+ upsertMarketContextBackfillCoverage: () => upsertMarketContextBackfillCoverage,
47
+ upsertMarketGlobalContextRows: () => upsertMarketGlobalContextRows,
48
+ upsertMarketReferenceAssetContextRows: () => upsertMarketReferenceAssetContextRows,
49
+ upsertMarketTradeFlowRows: () => upsertMarketTradeFlowRows
50
+ });
51
+ module.exports = __toCommonJS(marketContext_exports);
52
+
53
+ // src/timescale/pool.ts
54
+ var import_pg = require("pg");
55
+ var getPool = () => {
56
+ if (!global.__pgPool__) {
57
+ const max = Number(process.env.PG_POOL_MAX ?? 10);
58
+ const connectionTimeoutMillis = Number(
59
+ process.env.PG_CONNECTION_TIMEOUT_MS ?? 3e4
60
+ );
61
+ global.__pgPool__ = new import_pg.Pool({
62
+ host: process.env.PG_HOST || "127.0.0.1",
63
+ port: Number(process.env.PG_PORT ?? 5432),
64
+ user: process.env.PG_USER || "app",
65
+ password: String(process.env.PG_PASSWORD ?? "app"),
66
+ database: process.env.PG_DATABASE || process.env.PG_DB || "app",
67
+ max: Number.isFinite(max) && max > 0 ? Math.floor(max) : 10,
68
+ idleTimeoutMillis: 3e4,
69
+ connectionTimeoutMillis: Number.isFinite(connectionTimeoutMillis) && connectionTimeoutMillis > 0 ? Math.floor(connectionTimeoutMillis) : 3e4
70
+ });
71
+ }
72
+ return global.__pgPool__;
73
+ };
74
+
75
+ // src/timescale/query.ts
76
+ var resolveQueryTimeoutMs = (override) => {
77
+ if (Number.isFinite(override) && Number(override) > 0) {
78
+ return Math.floor(Number(override));
79
+ }
80
+ const configured = Number(process.env.MARKET_CONTEXT_SQL_TIMEOUT_MS);
81
+ return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 3e4;
82
+ };
83
+ var createQueryError = (name, message) => {
84
+ const error = new Error(message);
85
+ error.name = name;
86
+ return error;
87
+ };
88
+ var queryMarketContext = async (text, values, options = {}) => {
89
+ const client = await getPool().connect();
90
+ const timeoutMs = resolveQueryTimeoutMs(options.timeoutMs);
91
+ let released = false;
92
+ let rejectCancellation;
93
+ const release = (error) => {
94
+ if (released) return;
95
+ released = true;
96
+ client.release(error);
97
+ };
98
+ const cancellation = new Promise((_resolve, reject) => {
99
+ rejectCancellation = reject;
100
+ });
101
+ const cancel = (error) => {
102
+ release(error);
103
+ rejectCancellation?.(error);
104
+ };
105
+ const onAbort = () => cancel(
106
+ createQueryError("AbortError", "Timescale market-context query aborted")
107
+ );
108
+ const timer = setTimeout(
109
+ () => cancel(
110
+ createQueryError(
111
+ "TimescaleQueryTimeoutError",
112
+ `Timescale market-context query exceeded ${timeoutMs}ms`
113
+ )
114
+ ),
115
+ timeoutMs
116
+ );
117
+ timer.unref?.();
118
+ options.signal?.addEventListener("abort", onAbort, { once: true });
119
+ try {
120
+ if (options.signal?.aborted) {
121
+ const error = createQueryError(
122
+ "AbortError",
123
+ "Timescale market-context query aborted"
124
+ );
125
+ release(error);
126
+ throw error;
127
+ }
128
+ return await Promise.race([client.query(text, values), cancellation]);
129
+ } finally {
130
+ clearTimeout(timer);
131
+ options.signal?.removeEventListener("abort", onAbort);
132
+ release();
133
+ }
134
+ };
135
+
136
+ // src/timescale/values.ts
137
+ var PG_SAFE_MAX_BIND_PARAMS = 3e4;
138
+ var getSafeBulkInsertRows = (columnsCount) => Math.max(1, Math.floor(PG_SAFE_MAX_BIND_PARAMS / columnsCount));
139
+ var toMarketFeatureAge = (rowTs, atMs) => {
140
+ const ageMs = atMs - rowTs.getTime();
141
+ return Number.isFinite(ageMs) ? ageMs : null;
142
+ };
143
+
144
+ // src/timescale/internal.ts
145
+ var derivativesSchemaReady = false;
146
+ var binanceMarketSchemaReady = false;
147
+ var hyperliquidWhaleSchemaReady = false;
148
+ var derivativesSchemaReadyPromise = null;
149
+ var binanceMarketSchemaReadyPromise = null;
150
+ var hyperliquidWhaleSchemaReadyPromise = null;
151
+ var marketContextSchemaMode = "ensure";
152
+ var verifiedMarketContextSchemas = /* @__PURE__ */ new Set();
153
+ var DERIVATIVES_SCHEMA_LOCK_KEY = 610001;
154
+ var BINANCE_MARKET_SCHEMA_LOCK_KEY = 610003;
155
+ var HYPERLIQUID_WHALE_SCHEMA_LOCK_KEY = 610004;
156
+ var withSchemaLock = async (lockKey, work) => {
157
+ const pool = getPool();
158
+ await pool.query("SELECT pg_advisory_lock($1)", [lockKey]);
159
+ try {
160
+ await work();
161
+ } finally {
162
+ await pool.query("SELECT pg_advisory_unlock($1)", [lockKey]);
163
+ }
164
+ };
165
+ var ensureDerivativesSchema = async () => {
166
+ if (derivativesSchemaReady) return;
167
+ if (derivativesSchemaReadyPromise) {
168
+ await derivativesSchemaReadyPromise;
169
+ return;
170
+ }
171
+ const pool = getPool();
172
+ derivativesSchemaReadyPromise = withSchemaLock(
173
+ DERIVATIVES_SCHEMA_LOCK_KEY,
174
+ async () => {
175
+ if (derivativesSchemaReady) return;
176
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
177
+ await pool.query(`
178
+ CREATE TABLE IF NOT EXISTS derivatives_market (
179
+ symbol text NOT NULL,
180
+ interval text NOT NULL,
181
+ ts timestamptz NOT NULL,
182
+ open_interest double precision,
183
+ funding_rate double precision,
184
+ liq_long double precision,
185
+ liq_short double precision,
186
+ liq_total double precision,
187
+ source text,
188
+ ingested_at timestamptz NOT NULL DEFAULT now(),
189
+ PRIMARY KEY (symbol, interval, ts)
190
+ )
191
+ `);
192
+ await pool.query(`
193
+ SELECT create_hypertable(
194
+ 'derivatives_market',
195
+ 'ts',
196
+ if_not_exists => TRUE,
197
+ chunk_time_interval => interval '14 days'
198
+ )
199
+ `);
200
+ await pool.query(`
201
+ CREATE INDEX IF NOT EXISTS derivatives_market_symbol_tf_ts_idx
202
+ ON derivatives_market (symbol, interval, ts DESC)
203
+ `);
204
+ await pool.query(`
205
+ CREATE TABLE IF NOT EXISTS derivatives_backfill_coverage (
206
+ source text NOT NULL,
207
+ symbol text NOT NULL,
208
+ interval text NOT NULL,
209
+ from_ts timestamptz NOT NULL,
210
+ to_ts timestamptz NOT NULL,
211
+ rows_count integer NOT NULL DEFAULT 0,
212
+ checked_at timestamptz NOT NULL DEFAULT now(),
213
+ PRIMARY KEY (source, symbol, interval, from_ts, to_ts)
214
+ )
215
+ `);
216
+ await pool.query(`
217
+ CREATE INDEX IF NOT EXISTS derivatives_backfill_coverage_lookup_idx
218
+ ON derivatives_backfill_coverage (source, symbol, interval, from_ts, to_ts)
219
+ `);
220
+ await pool.query(`
221
+ CREATE TABLE IF NOT EXISTS derivatives_metric_coverage (
222
+ source text NOT NULL,
223
+ metric text NOT NULL,
224
+ symbol text NOT NULL,
225
+ interval text NOT NULL,
226
+ from_ts timestamptz NOT NULL,
227
+ to_ts timestamptz NOT NULL,
228
+ event_rows_count integer NOT NULL DEFAULT 0,
229
+ zero_rows_count integer NOT NULL DEFAULT 0,
230
+ checked_at timestamptz NOT NULL DEFAULT now(),
231
+ PRIMARY KEY (source, metric, symbol, interval, from_ts, to_ts)
232
+ )
233
+ `);
234
+ await pool.query(`
235
+ CREATE INDEX IF NOT EXISTS derivatives_metric_coverage_lookup_idx
236
+ ON derivatives_metric_coverage (
237
+ source,
238
+ metric,
239
+ symbol,
240
+ interval,
241
+ from_ts,
242
+ to_ts
243
+ )
244
+ `);
245
+ derivativesSchemaReady = true;
246
+ }
247
+ ).finally(() => {
248
+ derivativesSchemaReadyPromise = null;
249
+ });
250
+ await derivativesSchemaReadyPromise;
251
+ };
252
+ var ensureBinanceMarketSchema = async () => {
253
+ if (binanceMarketSchemaReady) return;
254
+ if (binanceMarketSchemaReadyPromise) {
255
+ await binanceMarketSchemaReadyPromise;
256
+ return;
257
+ }
258
+ const pool = getPool();
259
+ binanceMarketSchemaReadyPromise = withSchemaLock(
260
+ BINANCE_MARKET_SCHEMA_LOCK_KEY,
261
+ async () => {
262
+ if (binanceMarketSchemaReady) return;
263
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
264
+ await pool.query(`
265
+ CREATE TABLE IF NOT EXISTS market_trade_flow (
266
+ symbol text NOT NULL,
267
+ interval text NOT NULL,
268
+ ts timestamptz NOT NULL,
269
+ trades integer NOT NULL,
270
+ buy_base_volume double precision,
271
+ sell_base_volume double precision,
272
+ buy_quote_volume double precision,
273
+ sell_quote_volume double precision,
274
+ net_base_delta double precision,
275
+ net_quote_delta double precision,
276
+ buy_pressure_pct 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
+ 'market_trade_flow',
285
+ 'ts',
286
+ if_not_exists => TRUE,
287
+ chunk_time_interval => interval '7 days'
288
+ )
289
+ `);
290
+ await pool.query(`
291
+ CREATE INDEX IF NOT EXISTS market_trade_flow_symbol_tf_ts_idx
292
+ ON market_trade_flow (symbol, interval, ts DESC)
293
+ `);
294
+ await pool.query(`
295
+ CREATE TABLE IF NOT EXISTS market_breadth (
296
+ universe text NOT NULL,
297
+ interval text NOT NULL,
298
+ ts timestamptz NOT NULL,
299
+ symbols_count integer NOT NULL,
300
+ advancers integer NOT NULL,
301
+ decliners integer NOT NULL,
302
+ unchanged integer NOT NULL,
303
+ advance_decline_ratio double precision,
304
+ pct_above_ma20 double precision,
305
+ pct_above_ma50 double precision,
306
+ equal_weighted_return double precision,
307
+ volume_weighted_return double precision,
308
+ dispersion double precision,
309
+ btc_return_1h double precision,
310
+ btc_return_4h double precision,
311
+ btc_return_24h double precision,
312
+ alt_basket_return_1h double precision,
313
+ alt_basket_return_4h double precision,
314
+ alt_basket_return_24h double precision,
315
+ btc_vs_alt_return_1h double precision,
316
+ btc_vs_alt_return_4h double precision,
317
+ btc_vs_alt_return_24h double precision,
318
+ btc_turnover_share_1h double precision,
319
+ btc_turnover_share_24h double precision,
320
+ btc_turnover_share_change_24h double precision,
321
+ alt_vol_to_btc_vol_24h double precision,
322
+ alt_dispersion_24h double precision,
323
+ btc_alt_regime text,
324
+ source text,
325
+ ingested_at timestamptz NOT NULL DEFAULT now(),
326
+ PRIMARY KEY (universe, interval, ts)
327
+ )
328
+ `);
329
+ await pool.query(`
330
+ ALTER TABLE market_breadth
331
+ ADD COLUMN IF NOT EXISTS btc_return_1h double precision,
332
+ ADD COLUMN IF NOT EXISTS btc_return_4h double precision,
333
+ ADD COLUMN IF NOT EXISTS btc_return_24h double precision,
334
+ ADD COLUMN IF NOT EXISTS alt_basket_return_1h double precision,
335
+ ADD COLUMN IF NOT EXISTS alt_basket_return_4h double precision,
336
+ ADD COLUMN IF NOT EXISTS alt_basket_return_24h double precision,
337
+ ADD COLUMN IF NOT EXISTS btc_vs_alt_return_1h double precision,
338
+ ADD COLUMN IF NOT EXISTS btc_vs_alt_return_4h double precision,
339
+ ADD COLUMN IF NOT EXISTS btc_vs_alt_return_24h double precision,
340
+ ADD COLUMN IF NOT EXISTS btc_turnover_share_1h double precision,
341
+ ADD COLUMN IF NOT EXISTS btc_turnover_share_24h double precision,
342
+ ADD COLUMN IF NOT EXISTS btc_turnover_share_change_24h double precision,
343
+ ADD COLUMN IF NOT EXISTS alt_vol_to_btc_vol_24h double precision,
344
+ ADD COLUMN IF NOT EXISTS alt_dispersion_24h double precision,
345
+ ADD COLUMN IF NOT EXISTS btc_alt_regime text
346
+ `);
347
+ await pool.query(`
348
+ SELECT create_hypertable(
349
+ 'market_breadth',
350
+ 'ts',
351
+ if_not_exists => TRUE,
352
+ chunk_time_interval => interval '14 days'
353
+ )
354
+ `);
355
+ await pool.query(`
356
+ CREATE INDEX IF NOT EXISTS market_breadth_universe_tf_ts_idx
357
+ ON market_breadth (universe, interval, ts DESC)
358
+ `);
359
+ await pool.query(`
360
+ CREATE TABLE IF NOT EXISTS market_global_context (
361
+ source text NOT NULL,
362
+ ts timestamptz NOT NULL,
363
+ updated_at_ts timestamptz,
364
+ active_cryptocurrencies integer,
365
+ active_exchanges integer,
366
+ active_market_pairs integer,
367
+ markets integer,
368
+ total_market_cap_usd double precision,
369
+ total_volume_usd double precision,
370
+ total_volume_reported_usd double precision,
371
+ btc_dominance_pct double precision,
372
+ eth_dominance_pct double precision,
373
+ alt_market_cap_usd double precision,
374
+ alt_volume_usd double precision,
375
+ alt_volume_reported_usd double precision,
376
+ btc_to_alt_market_cap_ratio double precision,
377
+ market_cap_change_pct_24h_usd double precision,
378
+ ingested_at timestamptz NOT NULL DEFAULT now(),
379
+ PRIMARY KEY (source, ts)
380
+ )
381
+ `);
382
+ await pool.query(`
383
+ SELECT create_hypertable(
384
+ 'market_global_context',
385
+ 'ts',
386
+ if_not_exists => TRUE,
387
+ chunk_time_interval => interval '30 days'
388
+ )
389
+ `);
390
+ await pool.query(`
391
+ CREATE INDEX IF NOT EXISTS market_global_context_source_ts_idx
392
+ ON market_global_context (source, ts DESC)
393
+ `);
394
+ await pool.query(`
395
+ ALTER TABLE market_global_context
396
+ ADD COLUMN IF NOT EXISTS active_exchanges integer,
397
+ ADD COLUMN IF NOT EXISTS active_market_pairs integer,
398
+ ADD COLUMN IF NOT EXISTS total_volume_reported_usd double precision,
399
+ ADD COLUMN IF NOT EXISTS alt_volume_usd double precision,
400
+ ADD COLUMN IF NOT EXISTS alt_volume_reported_usd double precision
401
+ `);
402
+ await pool.query(`
403
+ CREATE TABLE IF NOT EXISTS market_reference_asset_context (
404
+ source text NOT NULL,
405
+ symbol text NOT NULL,
406
+ cmc_id integer NOT NULL,
407
+ interval text NOT NULL,
408
+ ts timestamptz NOT NULL,
409
+ open_usd double precision,
410
+ high_usd double precision,
411
+ low_usd double precision,
412
+ close_usd double precision,
413
+ volume_usd double precision,
414
+ market_cap_usd double precision,
415
+ ingested_at timestamptz NOT NULL DEFAULT now(),
416
+ PRIMARY KEY (source, symbol, interval, ts)
417
+ )
418
+ `);
419
+ await pool.query(`
420
+ SELECT create_hypertable(
421
+ 'market_reference_asset_context',
422
+ 'ts',
423
+ if_not_exists => TRUE,
424
+ chunk_time_interval => interval '30 days'
425
+ )
426
+ `);
427
+ await pool.query(`
428
+ CREATE INDEX IF NOT EXISTS market_reference_asset_context_lookup_idx
429
+ ON market_reference_asset_context (source, symbol, interval, ts DESC)
430
+ `);
431
+ await pool.query(`
432
+ CREATE TABLE IF NOT EXISTS market_cmc_exchange_liquidity_context (
433
+ source text NOT NULL,
434
+ interval text NOT NULL,
435
+ ts timestamptz NOT NULL,
436
+ exchanges_count integer NOT NULL,
437
+ total_volume_usd double precision,
438
+ binance_volume_usd double precision,
439
+ binance_volume_share double precision,
440
+ top_exchange_volume_share double precision,
441
+ liquidity_regime text,
442
+ ingested_at timestamptz NOT NULL DEFAULT now(),
443
+ PRIMARY KEY (source, interval, ts)
444
+ )
445
+ `);
446
+ await pool.query(`
447
+ SELECT create_hypertable(
448
+ 'market_cmc_exchange_liquidity_context',
449
+ 'ts',
450
+ if_not_exists => TRUE,
451
+ chunk_time_interval => interval '30 days'
452
+ )
453
+ `);
454
+ await pool.query(`
455
+ CREATE INDEX IF NOT EXISTS market_cmc_exchange_liquidity_context_lookup_idx
456
+ ON market_cmc_exchange_liquidity_context (source, interval, ts DESC)
457
+ `);
458
+ await pool.query(`
459
+ CREATE TABLE IF NOT EXISTS market_cmc_fear_greed_context (
460
+ source text NOT NULL,
461
+ interval text NOT NULL,
462
+ ts timestamptz NOT NULL,
463
+ value integer NOT NULL,
464
+ classification text NOT NULL,
465
+ sentiment_regime text NOT NULL,
466
+ ingested_at timestamptz NOT NULL DEFAULT now(),
467
+ PRIMARY KEY (source, interval, ts)
468
+ )
469
+ `);
470
+ await pool.query(`
471
+ SELECT create_hypertable(
472
+ 'market_cmc_fear_greed_context',
473
+ 'ts',
474
+ if_not_exists => TRUE,
475
+ chunk_time_interval => interval '30 days'
476
+ )
477
+ `);
478
+ await pool.query(`
479
+ CREATE INDEX IF NOT EXISTS market_cmc_fear_greed_context_lookup_idx
480
+ ON market_cmc_fear_greed_context (source, interval, ts DESC)
481
+ `);
482
+ await pool.query(`
483
+ CREATE TABLE IF NOT EXISTS market_cmc_index_context (
484
+ source text NOT NULL,
485
+ index_slug text NOT NULL,
486
+ interval text NOT NULL,
487
+ ts timestamptz NOT NULL,
488
+ value double precision NOT NULL,
489
+ constituents_count integer,
490
+ top_constituent_symbol text,
491
+ top_constituent_weight_pct double precision,
492
+ constituents jsonb,
493
+ ingested_at timestamptz NOT NULL DEFAULT now(),
494
+ PRIMARY KEY (source, index_slug, interval, ts)
495
+ )
496
+ `);
497
+ await pool.query(`
498
+ SELECT create_hypertable(
499
+ 'market_cmc_index_context',
500
+ 'ts',
501
+ if_not_exists => TRUE,
502
+ chunk_time_interval => interval '30 days'
503
+ )
504
+ `);
505
+ await pool.query(`
506
+ CREATE INDEX IF NOT EXISTS market_cmc_index_context_lookup_idx
507
+ ON market_cmc_index_context (source, index_slug, interval, ts DESC)
508
+ `);
509
+ await pool.query(`
510
+ CREATE TABLE IF NOT EXISTS market_context_backfill_coverage (
511
+ source text NOT NULL,
512
+ scope text NOT NULL,
513
+ interval text NOT NULL,
514
+ from_ts timestamptz NOT NULL,
515
+ to_ts timestamptz NOT NULL,
516
+ rows_count integer NOT NULL DEFAULT 0,
517
+ checked_at timestamptz NOT NULL DEFAULT now(),
518
+ PRIMARY KEY (source, scope, interval, from_ts, to_ts)
519
+ )
520
+ `);
521
+ await pool.query(`
522
+ CREATE INDEX IF NOT EXISTS market_context_backfill_coverage_lookup_idx
523
+ ON market_context_backfill_coverage (source, scope, interval, from_ts, to_ts)
524
+ `);
525
+ binanceMarketSchemaReady = true;
526
+ }
527
+ ).finally(() => {
528
+ binanceMarketSchemaReadyPromise = null;
529
+ });
530
+ await binanceMarketSchemaReadyPromise;
531
+ };
532
+ var ensureHyperliquidWhaleSchema = async () => {
533
+ if (hyperliquidWhaleSchemaReady) return;
534
+ if (hyperliquidWhaleSchemaReadyPromise) {
535
+ await hyperliquidWhaleSchemaReadyPromise;
536
+ return;
537
+ }
538
+ hyperliquidWhaleSchemaReadyPromise = withSchemaLock(
539
+ HYPERLIQUID_WHALE_SCHEMA_LOCK_KEY,
540
+ async () => {
541
+ if (hyperliquidWhaleSchemaReady) return;
542
+ const pool = getPool();
543
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
544
+ await pool.query(`
545
+ CREATE TABLE IF NOT EXISTS hyperliquid_whale_trade_events (
546
+ symbol text NOT NULL,
547
+ ts timestamptz NOT NULL,
548
+ tid text NOT NULL,
549
+ price double precision NOT NULL,
550
+ size double precision NOT NULL,
551
+ notional_usd double precision NOT NULL,
552
+ buyer_address text,
553
+ seller_address text,
554
+ buyer_tracked boolean NOT NULL,
555
+ seller_tracked boolean NOT NULL,
556
+ buyer_start_position double precision,
557
+ buyer_end_position double precision,
558
+ buyer_position_action text,
559
+ buyer_closed_pnl double precision,
560
+ buyer_liquidation boolean,
561
+ seller_start_position double precision,
562
+ seller_end_position double precision,
563
+ seller_position_action text,
564
+ seller_closed_pnl double precision,
565
+ seller_liquidation boolean,
566
+ universe_fingerprint text NOT NULL,
567
+ whale_registry_fingerprint text NOT NULL,
568
+ source text,
569
+ ingested_at timestamptz NOT NULL DEFAULT now(),
570
+ PRIMARY KEY (
571
+ universe_fingerprint,
572
+ whale_registry_fingerprint,
573
+ symbol,
574
+ ts,
575
+ tid
576
+ )
577
+ )
578
+ `);
579
+ await pool.query(`
580
+ ALTER TABLE hyperliquid_whale_trade_events
581
+ ADD COLUMN IF NOT EXISTS buyer_start_position double precision,
582
+ ADD COLUMN IF NOT EXISTS buyer_end_position double precision,
583
+ ADD COLUMN IF NOT EXISTS buyer_position_action text,
584
+ ADD COLUMN IF NOT EXISTS buyer_closed_pnl double precision,
585
+ ADD COLUMN IF NOT EXISTS buyer_liquidation boolean,
586
+ ADD COLUMN IF NOT EXISTS seller_start_position double precision,
587
+ ADD COLUMN IF NOT EXISTS seller_end_position double precision,
588
+ ADD COLUMN IF NOT EXISTS seller_position_action text,
589
+ ADD COLUMN IF NOT EXISTS seller_closed_pnl double precision,
590
+ ADD COLUMN IF NOT EXISTS seller_liquidation boolean
591
+ `);
592
+ await pool.query(`
593
+ SELECT create_hypertable(
594
+ 'hyperliquid_whale_trade_events',
595
+ 'ts',
596
+ if_not_exists => TRUE,
597
+ chunk_time_interval => interval '1 day'
598
+ )
599
+ `);
600
+ await pool.query(`
601
+ CREATE INDEX IF NOT EXISTS hyperliquid_whale_events_lookup_idx
602
+ ON hyperliquid_whale_trade_events (
603
+ universe_fingerprint,
604
+ whale_registry_fingerprint,
605
+ symbol,
606
+ ts DESC
607
+ )
608
+ `);
609
+ await pool.query(`
610
+ CREATE TABLE IF NOT EXISTS hyperliquid_whale_flow (
611
+ symbol text NOT NULL,
612
+ interval text NOT NULL,
613
+ ts timestamptz NOT NULL,
614
+ trades integer NOT NULL,
615
+ whale_sides integer NOT NULL,
616
+ unique_whales integer NOT NULL,
617
+ whale_addresses text[] NOT NULL DEFAULT '{}',
618
+ buy_notional_usd double precision NOT NULL,
619
+ sell_notional_usd double precision NOT NULL,
620
+ net_notional_usd double precision NOT NULL,
621
+ buy_share_pct double precision,
622
+ position_aware_whale_sides integer NOT NULL DEFAULT 0,
623
+ long_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
624
+ short_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
625
+ long_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
626
+ short_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
627
+ long_entry_notional_usd double precision NOT NULL DEFAULT 0,
628
+ short_entry_notional_usd double precision NOT NULL DEFAULT 0,
629
+ long_exit_notional_usd double precision NOT NULL DEFAULT 0,
630
+ short_exit_notional_usd double precision NOT NULL DEFAULT 0,
631
+ entry_net_notional_usd double precision NOT NULL DEFAULT 0,
632
+ entry_long_share_pct double precision,
633
+ universe_fingerprint text NOT NULL,
634
+ whale_registry_fingerprint text NOT NULL,
635
+ source text,
636
+ ingested_at timestamptz NOT NULL DEFAULT now(),
637
+ PRIMARY KEY (
638
+ universe_fingerprint,
639
+ whale_registry_fingerprint,
640
+ symbol,
641
+ interval,
642
+ ts
643
+ )
644
+ )
645
+ `);
646
+ await pool.query(`
647
+ ALTER TABLE hyperliquid_whale_flow
648
+ ADD COLUMN IF NOT EXISTS position_aware_whale_sides integer NOT NULL DEFAULT 0,
649
+ ADD COLUMN IF NOT EXISTS long_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
650
+ ADD COLUMN IF NOT EXISTS short_entry_whale_addresses text[] NOT NULL DEFAULT '{}',
651
+ ADD COLUMN IF NOT EXISTS long_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
652
+ ADD COLUMN IF NOT EXISTS short_exit_whale_addresses text[] NOT NULL DEFAULT '{}',
653
+ ADD COLUMN IF NOT EXISTS long_entry_notional_usd double precision NOT NULL DEFAULT 0,
654
+ ADD COLUMN IF NOT EXISTS short_entry_notional_usd double precision NOT NULL DEFAULT 0,
655
+ ADD COLUMN IF NOT EXISTS long_exit_notional_usd double precision NOT NULL DEFAULT 0,
656
+ ADD COLUMN IF NOT EXISTS short_exit_notional_usd double precision NOT NULL DEFAULT 0,
657
+ ADD COLUMN IF NOT EXISTS entry_net_notional_usd double precision NOT NULL DEFAULT 0,
658
+ ADD COLUMN IF NOT EXISTS entry_long_share_pct double precision
659
+ `);
660
+ await pool.query(`
661
+ SELECT create_hypertable(
662
+ 'hyperliquid_whale_flow',
663
+ 'ts',
664
+ if_not_exists => TRUE,
665
+ chunk_time_interval => interval '7 days'
666
+ )
667
+ `);
668
+ await pool.query(`
669
+ CREATE INDEX IF NOT EXISTS hyperliquid_whale_flow_lookup_idx
670
+ ON hyperliquid_whale_flow (
671
+ universe_fingerprint,
672
+ whale_registry_fingerprint,
673
+ symbol,
674
+ interval,
675
+ ts DESC
676
+ )
677
+ `);
678
+ await pool.query(`
679
+ CREATE TABLE IF NOT EXISTS hyperliquid_whale_wallet_coverage (
680
+ universe_fingerprint text NOT NULL,
681
+ whale_registry_fingerprint text NOT NULL,
682
+ address text NOT NULL,
683
+ requested_from_ts timestamptz NOT NULL,
684
+ requested_to_ts timestamptz NOT NULL,
685
+ covered_from_ts timestamptz,
686
+ covered_to_ts timestamptz,
687
+ status text NOT NULL CHECK (status IN ('complete', 'truncated', 'failed')),
688
+ fills_count integer NOT NULL DEFAULT 0,
689
+ error text,
690
+ data_model_version integer NOT NULL DEFAULT 2,
691
+ checked_at timestamptz NOT NULL DEFAULT now(),
692
+ PRIMARY KEY (
693
+ universe_fingerprint,
694
+ whale_registry_fingerprint,
695
+ address,
696
+ requested_from_ts,
697
+ requested_to_ts
698
+ )
699
+ )
700
+ `);
701
+ await pool.query(`
702
+ ALTER TABLE hyperliquid_whale_wallet_coverage
703
+ ADD COLUMN IF NOT EXISTS data_model_version integer NOT NULL DEFAULT 2
704
+ `);
705
+ await pool.query(`
706
+ CREATE INDEX IF NOT EXISTS hyperliquid_whale_wallet_coverage_lookup_idx
707
+ ON hyperliquid_whale_wallet_coverage (
708
+ universe_fingerprint,
709
+ whale_registry_fingerprint,
710
+ address,
711
+ requested_from_ts,
712
+ requested_to_ts
713
+ )
714
+ `);
715
+ await pool.query(`
716
+ CREATE TABLE IF NOT EXISTS hyperliquid_whale_coverage_1m (
717
+ ts timestamptz NOT NULL,
718
+ covered_whales integer NOT NULL,
719
+ expected_whales integer NOT NULL,
720
+ coverage_pct double precision NOT NULL,
721
+ universe_fingerprint text NOT NULL,
722
+ whale_registry_fingerprint text NOT NULL,
723
+ source text,
724
+ data_model_version integer NOT NULL DEFAULT 2,
725
+ ingested_at timestamptz NOT NULL DEFAULT now(),
726
+ PRIMARY KEY (
727
+ universe_fingerprint,
728
+ whale_registry_fingerprint,
729
+ ts
730
+ )
731
+ )
732
+ `);
733
+ await pool.query(`
734
+ ALTER TABLE hyperliquid_whale_coverage_1m
735
+ ADD COLUMN IF NOT EXISTS data_model_version integer NOT NULL DEFAULT 2
736
+ `);
737
+ await pool.query(`
738
+ SELECT create_hypertable(
739
+ 'hyperliquid_whale_coverage_1m',
740
+ 'ts',
741
+ if_not_exists => TRUE,
742
+ chunk_time_interval => interval '7 days'
743
+ )
744
+ `);
745
+ await pool.query(`
746
+ CREATE INDEX IF NOT EXISTS hyperliquid_whale_coverage_1m_lookup_idx
747
+ ON hyperliquid_whale_coverage_1m (
748
+ universe_fingerprint,
749
+ whale_registry_fingerprint,
750
+ ts DESC
751
+ )
752
+ `);
753
+ hyperliquidWhaleSchemaReady = true;
754
+ }
755
+ ).finally(() => {
756
+ hyperliquidWhaleSchemaReadyPromise = null;
757
+ });
758
+ await hyperliquidWhaleSchemaReadyPromise;
759
+ };
760
+ var ensureCoinMarketCapContextSchema = async () => ensureBinanceMarketSchema();
761
+ var ensureMarketContextSchema = async (source) => {
762
+ switch (source) {
763
+ case "binance":
764
+ return ensureBinanceMarketSchema();
765
+ case "coinmarketcap":
766
+ return ensureCoinMarketCapContextSchema();
767
+ case "derivatives":
768
+ return ensureDerivativesSchema();
769
+ case "hyperliquidWhales":
770
+ return ensureHyperliquidWhaleSchema();
771
+ }
772
+ };
773
+ var MARKET_CONTEXT_SCHEMA_TABLES = {
774
+ binance: ["market_trade_flow", "market_breadth"],
775
+ coinmarketcap: [
776
+ "market_global_context",
777
+ "market_reference_asset_context",
778
+ "market_cmc_exchange_liquidity_context",
779
+ "market_cmc_fear_greed_context",
780
+ "market_cmc_index_context"
781
+ ],
782
+ derivatives: ["derivatives_market"],
783
+ hyperliquidWhales: [
784
+ "hyperliquid_whale_flow",
785
+ "hyperliquid_whale_coverage_1m"
786
+ ]
787
+ };
788
+ var verifyMarketContextSchema = async (source) => {
789
+ if (verifiedMarketContextSchemas.has(source)) return;
790
+ const tables = MARKET_CONTEXT_SCHEMA_TABLES[source];
791
+ const result = await queryMarketContext(
792
+ `
793
+ SELECT table_name AS "tableName"
794
+ FROM unnest($1::text[]) AS requested(table_name)
795
+ WHERE to_regclass(requested.table_name) IS NULL
796
+ `,
797
+ [tables]
798
+ );
799
+ if (result.rows.length) {
800
+ throw new Error(
801
+ `Timescale ${source} schema is not prepared; missing: ${result.rows.map((row) => row.tableName).filter(Boolean).join(", ")}`
802
+ );
803
+ }
804
+ verifiedMarketContextSchemas.add(source);
805
+ };
806
+ var prepareMarketContextSchemaForRead = async (source) => marketContextSchemaMode === "verify" ? verifyMarketContextSchema(source) : ensureMarketContextSchema(source);
807
+ var ensureMarketContextSchemas = async (sources) => {
808
+ for (const source of new Set(sources)) {
809
+ await ensureMarketContextSchema(source);
810
+ }
811
+ };
812
+
813
+ // src/timescale/marketContext.ts
814
+ async function upsertMarketTradeFlowRows(rows) {
815
+ if (!rows.length) return;
816
+ await ensureBinanceMarketSchema();
817
+ const pool = getPool();
818
+ const cols = [
819
+ "symbol",
820
+ "interval",
821
+ "ts",
822
+ "trades",
823
+ "buy_base_volume",
824
+ "sell_base_volume",
825
+ "buy_quote_volume",
826
+ "sell_quote_volume",
827
+ "net_base_delta",
828
+ "net_quote_delta",
829
+ "buy_pressure_pct",
830
+ "source"
831
+ ];
832
+ const maxRows = getSafeBulkInsertRows(cols.length);
833
+ if (rows.length > maxRows) {
834
+ for (let i = 0; i < rows.length; i += maxRows) {
835
+ await upsertMarketTradeFlowRows(rows.slice(i, i + maxRows));
836
+ }
837
+ return;
838
+ }
839
+ const valuesSql = rows.map(
840
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
841
+ ).join(",");
842
+ const flat = rows.flatMap((row) => [
843
+ row.symbol,
844
+ row.interval,
845
+ row.ts,
846
+ row.trades,
847
+ row.buyBaseVolume ?? null,
848
+ row.sellBaseVolume ?? null,
849
+ row.buyQuoteVolume ?? null,
850
+ row.sellQuoteVolume ?? null,
851
+ row.netBaseDelta ?? null,
852
+ row.netQuoteDelta ?? null,
853
+ row.buyPressurePct ?? null,
854
+ row.source ?? null
855
+ ]);
856
+ await pool.query(
857
+ `
858
+ INSERT INTO market_trade_flow (${cols.join(",")})
859
+ VALUES ${valuesSql}
860
+ ON CONFLICT (symbol, interval, ts) DO UPDATE SET
861
+ trades = EXCLUDED.trades,
862
+ buy_base_volume = COALESCE(EXCLUDED.buy_base_volume, market_trade_flow.buy_base_volume),
863
+ sell_base_volume = COALESCE(EXCLUDED.sell_base_volume, market_trade_flow.sell_base_volume),
864
+ buy_quote_volume = COALESCE(EXCLUDED.buy_quote_volume, market_trade_flow.buy_quote_volume),
865
+ sell_quote_volume = COALESCE(EXCLUDED.sell_quote_volume, market_trade_flow.sell_quote_volume),
866
+ net_base_delta = COALESCE(EXCLUDED.net_base_delta, market_trade_flow.net_base_delta),
867
+ net_quote_delta = COALESCE(EXCLUDED.net_quote_delta, market_trade_flow.net_quote_delta),
868
+ buy_pressure_pct = COALESCE(EXCLUDED.buy_pressure_pct, market_trade_flow.buy_pressure_pct),
869
+ source = COALESCE(EXCLUDED.source, market_trade_flow.source),
870
+ ingested_at = now()
871
+ `,
872
+ flat
873
+ );
874
+ }
875
+ async function upsertMarketBreadthRows(rows) {
876
+ if (!rows.length) return;
877
+ await ensureBinanceMarketSchema();
878
+ const pool = getPool();
879
+ const cols = [
880
+ "universe",
881
+ "interval",
882
+ "ts",
883
+ "symbols_count",
884
+ "advancers",
885
+ "decliners",
886
+ "unchanged",
887
+ "advance_decline_ratio",
888
+ "pct_above_ma20",
889
+ "pct_above_ma50",
890
+ "equal_weighted_return",
891
+ "volume_weighted_return",
892
+ "dispersion",
893
+ "btc_return_1h",
894
+ "btc_return_4h",
895
+ "btc_return_24h",
896
+ "alt_basket_return_1h",
897
+ "alt_basket_return_4h",
898
+ "alt_basket_return_24h",
899
+ "btc_vs_alt_return_1h",
900
+ "btc_vs_alt_return_4h",
901
+ "btc_vs_alt_return_24h",
902
+ "btc_turnover_share_1h",
903
+ "btc_turnover_share_24h",
904
+ "btc_turnover_share_change_24h",
905
+ "alt_vol_to_btc_vol_24h",
906
+ "alt_dispersion_24h",
907
+ "btc_alt_regime",
908
+ "source"
909
+ ];
910
+ const maxRows = getSafeBulkInsertRows(cols.length);
911
+ if (rows.length > maxRows) {
912
+ for (let i = 0; i < rows.length; i += maxRows) {
913
+ await upsertMarketBreadthRows(rows.slice(i, i + maxRows));
914
+ }
915
+ return;
916
+ }
917
+ const valuesSql = rows.map(
918
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
919
+ ).join(",");
920
+ const flat = rows.flatMap((row) => [
921
+ row.universe,
922
+ row.interval,
923
+ row.ts,
924
+ row.symbolsCount,
925
+ row.advancers,
926
+ row.decliners,
927
+ row.unchanged,
928
+ row.advanceDeclineRatio ?? null,
929
+ row.pctAboveMa20 ?? null,
930
+ row.pctAboveMa50 ?? null,
931
+ row.equalWeightedReturn ?? null,
932
+ row.volumeWeightedReturn ?? null,
933
+ row.dispersion ?? null,
934
+ row.btcReturn1h ?? null,
935
+ row.btcReturn4h ?? null,
936
+ row.btcReturn24h ?? null,
937
+ row.altBasketReturn1h ?? null,
938
+ row.altBasketReturn4h ?? null,
939
+ row.altBasketReturn24h ?? null,
940
+ row.btcVsAltReturn1h ?? null,
941
+ row.btcVsAltReturn4h ?? null,
942
+ row.btcVsAltReturn24h ?? null,
943
+ row.btcTurnoverShare1h ?? null,
944
+ row.btcTurnoverShare24h ?? null,
945
+ row.btcTurnoverShareChange24h ?? null,
946
+ row.altVolToBtcVol24h ?? null,
947
+ row.altDispersion24h ?? null,
948
+ row.btcAltRegime ?? null,
949
+ row.source ?? null
950
+ ]);
951
+ await pool.query(
952
+ `
953
+ INSERT INTO market_breadth (${cols.join(",")})
954
+ VALUES ${valuesSql}
955
+ ON CONFLICT (universe, interval, ts) DO UPDATE SET
956
+ symbols_count = EXCLUDED.symbols_count,
957
+ advancers = EXCLUDED.advancers,
958
+ decliners = EXCLUDED.decliners,
959
+ unchanged = EXCLUDED.unchanged,
960
+ advance_decline_ratio = COALESCE(EXCLUDED.advance_decline_ratio, market_breadth.advance_decline_ratio),
961
+ pct_above_ma20 = COALESCE(EXCLUDED.pct_above_ma20, market_breadth.pct_above_ma20),
962
+ pct_above_ma50 = COALESCE(EXCLUDED.pct_above_ma50, market_breadth.pct_above_ma50),
963
+ equal_weighted_return = COALESCE(EXCLUDED.equal_weighted_return, market_breadth.equal_weighted_return),
964
+ volume_weighted_return = COALESCE(EXCLUDED.volume_weighted_return, market_breadth.volume_weighted_return),
965
+ dispersion = COALESCE(EXCLUDED.dispersion, market_breadth.dispersion),
966
+ btc_return_1h = COALESCE(EXCLUDED.btc_return_1h, market_breadth.btc_return_1h),
967
+ btc_return_4h = COALESCE(EXCLUDED.btc_return_4h, market_breadth.btc_return_4h),
968
+ btc_return_24h = COALESCE(EXCLUDED.btc_return_24h, market_breadth.btc_return_24h),
969
+ alt_basket_return_1h = COALESCE(EXCLUDED.alt_basket_return_1h, market_breadth.alt_basket_return_1h),
970
+ alt_basket_return_4h = COALESCE(EXCLUDED.alt_basket_return_4h, market_breadth.alt_basket_return_4h),
971
+ alt_basket_return_24h = COALESCE(EXCLUDED.alt_basket_return_24h, market_breadth.alt_basket_return_24h),
972
+ btc_vs_alt_return_1h = COALESCE(EXCLUDED.btc_vs_alt_return_1h, market_breadth.btc_vs_alt_return_1h),
973
+ btc_vs_alt_return_4h = COALESCE(EXCLUDED.btc_vs_alt_return_4h, market_breadth.btc_vs_alt_return_4h),
974
+ btc_vs_alt_return_24h = COALESCE(EXCLUDED.btc_vs_alt_return_24h, market_breadth.btc_vs_alt_return_24h),
975
+ btc_turnover_share_1h = COALESCE(EXCLUDED.btc_turnover_share_1h, market_breadth.btc_turnover_share_1h),
976
+ btc_turnover_share_24h = COALESCE(EXCLUDED.btc_turnover_share_24h, market_breadth.btc_turnover_share_24h),
977
+ btc_turnover_share_change_24h = COALESCE(EXCLUDED.btc_turnover_share_change_24h, market_breadth.btc_turnover_share_change_24h),
978
+ alt_vol_to_btc_vol_24h = COALESCE(EXCLUDED.alt_vol_to_btc_vol_24h, market_breadth.alt_vol_to_btc_vol_24h),
979
+ alt_dispersion_24h = COALESCE(EXCLUDED.alt_dispersion_24h, market_breadth.alt_dispersion_24h),
980
+ btc_alt_regime = COALESCE(EXCLUDED.btc_alt_regime, market_breadth.btc_alt_regime),
981
+ source = COALESCE(EXCLUDED.source, market_breadth.source),
982
+ ingested_at = now()
983
+ `,
984
+ flat
985
+ );
986
+ }
987
+ async function upsertMarketGlobalContextRows(rows) {
988
+ if (!rows.length) return;
989
+ await ensureBinanceMarketSchema();
990
+ const pool = getPool();
991
+ const cols = [
992
+ "source",
993
+ "ts",
994
+ "updated_at_ts",
995
+ "active_cryptocurrencies",
996
+ "active_exchanges",
997
+ "active_market_pairs",
998
+ "markets",
999
+ "total_market_cap_usd",
1000
+ "total_volume_usd",
1001
+ "total_volume_reported_usd",
1002
+ "btc_dominance_pct",
1003
+ "eth_dominance_pct",
1004
+ "alt_market_cap_usd",
1005
+ "alt_volume_usd",
1006
+ "alt_volume_reported_usd",
1007
+ "btc_to_alt_market_cap_ratio",
1008
+ "market_cap_change_pct_24h_usd"
1009
+ ];
1010
+ const maxRows = getSafeBulkInsertRows(cols.length);
1011
+ if (rows.length > maxRows) {
1012
+ for (let i = 0; i < rows.length; i += maxRows) {
1013
+ await upsertMarketGlobalContextRows(rows.slice(i, i + maxRows));
1014
+ }
1015
+ return;
1016
+ }
1017
+ const valuesSql = rows.map(
1018
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1019
+ ).join(",");
1020
+ const flat = rows.flatMap((row) => [
1021
+ row.source,
1022
+ row.ts,
1023
+ row.updatedAt ?? null,
1024
+ row.activeCryptocurrencies ?? null,
1025
+ row.activeExchanges ?? null,
1026
+ row.activeMarketPairs ?? null,
1027
+ row.markets ?? null,
1028
+ row.totalMarketCapUsd ?? null,
1029
+ row.totalVolumeUsd ?? null,
1030
+ row.totalVolumeReportedUsd ?? null,
1031
+ row.btcDominancePct ?? null,
1032
+ row.ethDominancePct ?? null,
1033
+ row.altMarketCapUsd ?? null,
1034
+ row.altVolumeUsd ?? null,
1035
+ row.altVolumeReportedUsd ?? null,
1036
+ row.btcToAltMarketCapRatio ?? null,
1037
+ row.marketCapChangePct24hUsd ?? null
1038
+ ]);
1039
+ await pool.query(
1040
+ `
1041
+ INSERT INTO market_global_context (${cols.join(",")})
1042
+ VALUES ${valuesSql}
1043
+ ON CONFLICT (source, ts) DO UPDATE SET
1044
+ updated_at_ts = COALESCE(EXCLUDED.updated_at_ts, market_global_context.updated_at_ts),
1045
+ active_cryptocurrencies = COALESCE(EXCLUDED.active_cryptocurrencies, market_global_context.active_cryptocurrencies),
1046
+ active_exchanges = COALESCE(EXCLUDED.active_exchanges, market_global_context.active_exchanges),
1047
+ active_market_pairs = COALESCE(EXCLUDED.active_market_pairs, market_global_context.active_market_pairs),
1048
+ markets = COALESCE(EXCLUDED.markets, market_global_context.markets),
1049
+ total_market_cap_usd = COALESCE(EXCLUDED.total_market_cap_usd, market_global_context.total_market_cap_usd),
1050
+ total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_global_context.total_volume_usd),
1051
+ total_volume_reported_usd = COALESCE(EXCLUDED.total_volume_reported_usd, market_global_context.total_volume_reported_usd),
1052
+ btc_dominance_pct = COALESCE(EXCLUDED.btc_dominance_pct, market_global_context.btc_dominance_pct),
1053
+ eth_dominance_pct = COALESCE(EXCLUDED.eth_dominance_pct, market_global_context.eth_dominance_pct),
1054
+ alt_market_cap_usd = COALESCE(EXCLUDED.alt_market_cap_usd, market_global_context.alt_market_cap_usd),
1055
+ alt_volume_usd = COALESCE(EXCLUDED.alt_volume_usd, market_global_context.alt_volume_usd),
1056
+ alt_volume_reported_usd = COALESCE(EXCLUDED.alt_volume_reported_usd, market_global_context.alt_volume_reported_usd),
1057
+ btc_to_alt_market_cap_ratio = COALESCE(EXCLUDED.btc_to_alt_market_cap_ratio, market_global_context.btc_to_alt_market_cap_ratio),
1058
+ market_cap_change_pct_24h_usd = COALESCE(EXCLUDED.market_cap_change_pct_24h_usd, market_global_context.market_cap_change_pct_24h_usd),
1059
+ ingested_at = now()
1060
+ `,
1061
+ flat
1062
+ );
1063
+ }
1064
+ async function upsertMarketReferenceAssetContextRows(rows) {
1065
+ if (!rows.length) return;
1066
+ await ensureBinanceMarketSchema();
1067
+ const pool = getPool();
1068
+ const cols = [
1069
+ "source",
1070
+ "symbol",
1071
+ "cmc_id",
1072
+ "interval",
1073
+ "ts",
1074
+ "open_usd",
1075
+ "high_usd",
1076
+ "low_usd",
1077
+ "close_usd",
1078
+ "volume_usd",
1079
+ "market_cap_usd"
1080
+ ];
1081
+ const maxRows = getSafeBulkInsertRows(cols.length);
1082
+ if (rows.length > maxRows) {
1083
+ for (let i = 0; i < rows.length; i += maxRows) {
1084
+ await upsertMarketReferenceAssetContextRows(rows.slice(i, i + maxRows));
1085
+ }
1086
+ return;
1087
+ }
1088
+ const valuesSql = rows.map(
1089
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1090
+ ).join(",");
1091
+ const flat = rows.flatMap((row) => [
1092
+ row.source,
1093
+ row.symbol.trim().toUpperCase(),
1094
+ Math.trunc(row.cmcId),
1095
+ row.interval,
1096
+ row.ts,
1097
+ row.openUsd ?? null,
1098
+ row.highUsd ?? null,
1099
+ row.lowUsd ?? null,
1100
+ row.closeUsd ?? null,
1101
+ row.volumeUsd ?? null,
1102
+ row.marketCapUsd ?? null
1103
+ ]);
1104
+ await pool.query(
1105
+ `
1106
+ INSERT INTO market_reference_asset_context (${cols.join(",")})
1107
+ VALUES ${valuesSql}
1108
+ ON CONFLICT (source, symbol, interval, ts) DO UPDATE SET
1109
+ cmc_id = EXCLUDED.cmc_id,
1110
+ open_usd = COALESCE(EXCLUDED.open_usd, market_reference_asset_context.open_usd),
1111
+ high_usd = COALESCE(EXCLUDED.high_usd, market_reference_asset_context.high_usd),
1112
+ low_usd = COALESCE(EXCLUDED.low_usd, market_reference_asset_context.low_usd),
1113
+ close_usd = COALESCE(EXCLUDED.close_usd, market_reference_asset_context.close_usd),
1114
+ volume_usd = COALESCE(EXCLUDED.volume_usd, market_reference_asset_context.volume_usd),
1115
+ market_cap_usd = COALESCE(EXCLUDED.market_cap_usd, market_reference_asset_context.market_cap_usd),
1116
+ ingested_at = now()
1117
+ `,
1118
+ flat
1119
+ );
1120
+ }
1121
+ async function upsertMarketCmcExchangeLiquidityContextRows(rows) {
1122
+ if (!rows.length) return;
1123
+ await ensureBinanceMarketSchema();
1124
+ const pool = getPool();
1125
+ const cols = [
1126
+ "source",
1127
+ "interval",
1128
+ "ts",
1129
+ "exchanges_count",
1130
+ "total_volume_usd",
1131
+ "binance_volume_usd",
1132
+ "binance_volume_share",
1133
+ "top_exchange_volume_share",
1134
+ "liquidity_regime"
1135
+ ];
1136
+ const maxRows = getSafeBulkInsertRows(cols.length);
1137
+ if (rows.length > maxRows) {
1138
+ for (let i = 0; i < rows.length; i += maxRows) {
1139
+ await upsertMarketCmcExchangeLiquidityContextRows(
1140
+ rows.slice(i, i + maxRows)
1141
+ );
1142
+ }
1143
+ return;
1144
+ }
1145
+ const valuesSql = rows.map(
1146
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1147
+ ).join(",");
1148
+ const flat = rows.flatMap((row) => [
1149
+ row.source,
1150
+ row.interval,
1151
+ row.ts,
1152
+ Math.trunc(row.exchangesCount),
1153
+ row.totalVolumeUsd ?? null,
1154
+ row.binanceVolumeUsd ?? null,
1155
+ row.binanceVolumeShare ?? null,
1156
+ row.topExchangeVolumeShare ?? null,
1157
+ row.liquidityRegime ?? null
1158
+ ]);
1159
+ await pool.query(
1160
+ `
1161
+ INSERT INTO market_cmc_exchange_liquidity_context (${cols.join(",")})
1162
+ VALUES ${valuesSql}
1163
+ ON CONFLICT (source, interval, ts) DO UPDATE SET
1164
+ exchanges_count = EXCLUDED.exchanges_count,
1165
+ total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_cmc_exchange_liquidity_context.total_volume_usd),
1166
+ binance_volume_usd = COALESCE(EXCLUDED.binance_volume_usd, market_cmc_exchange_liquidity_context.binance_volume_usd),
1167
+ binance_volume_share = COALESCE(EXCLUDED.binance_volume_share, market_cmc_exchange_liquidity_context.binance_volume_share),
1168
+ top_exchange_volume_share = COALESCE(EXCLUDED.top_exchange_volume_share, market_cmc_exchange_liquidity_context.top_exchange_volume_share),
1169
+ liquidity_regime = COALESCE(EXCLUDED.liquidity_regime, market_cmc_exchange_liquidity_context.liquidity_regime),
1170
+ ingested_at = now()
1171
+ `,
1172
+ flat
1173
+ );
1174
+ }
1175
+ async function upsertMarketCmcFearGreedContextRows(rows) {
1176
+ if (!rows.length) return;
1177
+ await ensureBinanceMarketSchema();
1178
+ const pool = getPool();
1179
+ const cols = [
1180
+ "source",
1181
+ "interval",
1182
+ "ts",
1183
+ "value",
1184
+ "classification",
1185
+ "sentiment_regime"
1186
+ ];
1187
+ const maxRows = getSafeBulkInsertRows(cols.length);
1188
+ if (rows.length > maxRows) {
1189
+ for (let i = 0; i < rows.length; i += maxRows) {
1190
+ await upsertMarketCmcFearGreedContextRows(rows.slice(i, i + maxRows));
1191
+ }
1192
+ return;
1193
+ }
1194
+ const valuesSql = rows.map(
1195
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1196
+ ).join(",");
1197
+ const flat = rows.flatMap((row) => [
1198
+ row.source,
1199
+ row.interval,
1200
+ row.ts,
1201
+ Math.trunc(row.value),
1202
+ row.classification,
1203
+ row.sentimentRegime
1204
+ ]);
1205
+ await pool.query(
1206
+ `
1207
+ INSERT INTO market_cmc_fear_greed_context (${cols.join(",")})
1208
+ VALUES ${valuesSql}
1209
+ ON CONFLICT (source, interval, ts) DO UPDATE SET
1210
+ value = EXCLUDED.value,
1211
+ classification = EXCLUDED.classification,
1212
+ sentiment_regime = EXCLUDED.sentiment_regime,
1213
+ ingested_at = now()
1214
+ `,
1215
+ flat
1216
+ );
1217
+ }
1218
+ async function upsertMarketCmcIndexContextRows(rows) {
1219
+ if (!rows.length) return;
1220
+ await ensureBinanceMarketSchema();
1221
+ const pool = getPool();
1222
+ const cols = [
1223
+ "source",
1224
+ "index_slug",
1225
+ "interval",
1226
+ "ts",
1227
+ "value",
1228
+ "constituents_count",
1229
+ "top_constituent_symbol",
1230
+ "top_constituent_weight_pct",
1231
+ "constituents"
1232
+ ];
1233
+ const maxRows = getSafeBulkInsertRows(cols.length);
1234
+ if (rows.length > maxRows) {
1235
+ for (let i = 0; i < rows.length; i += maxRows) {
1236
+ await upsertMarketCmcIndexContextRows(rows.slice(i, i + maxRows));
1237
+ }
1238
+ return;
1239
+ }
1240
+ const valuesSql = rows.map(
1241
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1242
+ ).join(",");
1243
+ const flat = rows.flatMap((row) => [
1244
+ row.source,
1245
+ row.indexSlug,
1246
+ row.interval,
1247
+ row.ts,
1248
+ row.value,
1249
+ row.constituentsCount ?? null,
1250
+ row.topConstituentSymbol ?? null,
1251
+ row.topConstituentWeightPct ?? null,
1252
+ row.constituents ? JSON.stringify(row.constituents) : null
1253
+ ]);
1254
+ await pool.query(
1255
+ `
1256
+ INSERT INTO market_cmc_index_context (${cols.join(",")})
1257
+ VALUES ${valuesSql}
1258
+ ON CONFLICT (source, index_slug, interval, ts) DO UPDATE SET
1259
+ value = EXCLUDED.value,
1260
+ constituents_count = COALESCE(EXCLUDED.constituents_count, market_cmc_index_context.constituents_count),
1261
+ top_constituent_symbol = COALESCE(EXCLUDED.top_constituent_symbol, market_cmc_index_context.top_constituent_symbol),
1262
+ top_constituent_weight_pct = COALESCE(EXCLUDED.top_constituent_weight_pct, market_cmc_index_context.top_constituent_weight_pct),
1263
+ constituents = COALESCE(EXCLUDED.constituents, market_cmc_index_context.constituents),
1264
+ ingested_at = now()
1265
+ `,
1266
+ flat
1267
+ );
1268
+ }
1269
+ async function getMarketContextBackfillCoverage(params) {
1270
+ const source = String(params.source || "").trim().toLowerCase();
1271
+ const scopes = [
1272
+ ...new Set(
1273
+ params.scopes.map(
1274
+ (scope) => String(scope || "").trim().toLowerCase()
1275
+ ).filter(Boolean)
1276
+ )
1277
+ ];
1278
+ const interval = String(params.interval || "").trim().toLowerCase();
1279
+ if (!source || !scopes.length || !interval) return [];
1280
+ await ensureBinanceMarketSchema();
1281
+ const pool = getPool();
1282
+ const res = await pool.query(
1283
+ `
1284
+ SELECT
1285
+ source,
1286
+ scope,
1287
+ interval,
1288
+ extract(epoch from from_ts)*1000 AS from_ms,
1289
+ extract(epoch from to_ts)*1000 AS to_ms,
1290
+ rows_count,
1291
+ extract(epoch from checked_at)*1000 AS checked_at_ms
1292
+ FROM market_context_backfill_coverage
1293
+ WHERE source = $1
1294
+ AND scope = ANY($2)
1295
+ AND interval = $3
1296
+ AND from_ts >= to_timestamp($4/1000.0)
1297
+ AND to_ts <= to_timestamp($5/1000.0)
1298
+ `,
1299
+ [source, scopes, interval, params.fromMs, params.toMs]
1300
+ );
1301
+ return res.rows.map((row) => {
1302
+ const checkedAtMs = Number(row.checked_at_ms);
1303
+ return {
1304
+ source: String(row.source).toLowerCase(),
1305
+ scope: String(row.scope).toLowerCase(),
1306
+ interval: String(row.interval).toLowerCase(),
1307
+ fromMs: Number(row.from_ms),
1308
+ toMs: Number(row.to_ms),
1309
+ rowsCount: Number(row.rows_count ?? 0),
1310
+ ...Number.isFinite(checkedAtMs) ? { checkedAtMs } : {}
1311
+ };
1312
+ });
1313
+ }
1314
+ async function upsertMarketContextBackfillCoverage(rows) {
1315
+ const normalizedRows = rows.map((row) => ({
1316
+ source: String(row.source || "").trim().toLowerCase(),
1317
+ scope: String(row.scope || "").trim().toLowerCase(),
1318
+ interval: String(row.interval || "").trim().toLowerCase(),
1319
+ fromMs: Math.trunc(row.fromMs),
1320
+ toMs: Math.trunc(row.toMs),
1321
+ rowsCount: Math.max(0, Math.trunc(row.rowsCount))
1322
+ })).filter(
1323
+ (row) => row.source && row.scope && row.interval && Number.isFinite(row.fromMs) && Number.isFinite(row.toMs) && row.toMs >= row.fromMs
1324
+ );
1325
+ if (!normalizedRows.length) return;
1326
+ await ensureBinanceMarketSchema();
1327
+ const pool = getPool();
1328
+ const cols = [
1329
+ "source",
1330
+ "scope",
1331
+ "interval",
1332
+ "from_ts",
1333
+ "to_ts",
1334
+ "rows_count"
1335
+ ];
1336
+ const valuesSql = normalizedRows.map(
1337
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1338
+ ).join(",");
1339
+ const flat = normalizedRows.flatMap((row) => [
1340
+ row.source,
1341
+ row.scope,
1342
+ row.interval,
1343
+ new Date(row.fromMs),
1344
+ new Date(row.toMs),
1345
+ row.rowsCount
1346
+ ]);
1347
+ await pool.query(
1348
+ `
1349
+ INSERT INTO market_context_backfill_coverage (${cols.join(",")})
1350
+ VALUES ${valuesSql}
1351
+ ON CONFLICT (source, scope, interval, from_ts, to_ts) DO UPDATE SET
1352
+ rows_count = EXCLUDED.rows_count,
1353
+ checked_at = now()
1354
+ `,
1355
+ flat
1356
+ );
1357
+ }
1358
+ async function getLatestMarketTradeFlow(params) {
1359
+ await prepareMarketContextSchemaForRead("binance");
1360
+ const res = await queryMarketContext(
1361
+ `
1362
+ SELECT
1363
+ symbol,
1364
+ interval,
1365
+ ts,
1366
+ trades::int AS trades,
1367
+ buy_base_volume AS "buyBaseVolume",
1368
+ sell_base_volume AS "sellBaseVolume",
1369
+ buy_quote_volume AS "buyQuoteVolume",
1370
+ sell_quote_volume AS "sellQuoteVolume",
1371
+ net_base_delta AS "netBaseDelta",
1372
+ net_quote_delta AS "netQuoteDelta",
1373
+ buy_pressure_pct AS "buyPressurePct",
1374
+ source
1375
+ FROM market_trade_flow
1376
+ WHERE symbol = $1
1377
+ AND interval = $2
1378
+ AND ts <= to_timestamp($3/1000.0)
1379
+ ORDER BY ts DESC
1380
+ LIMIT 1
1381
+ `,
1382
+ [params.symbol.toUpperCase(), params.interval, params.atMs],
1383
+ params
1384
+ );
1385
+ const row = res.rows[0];
1386
+ if (!row) return null;
1387
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1388
+ return {
1389
+ ...row,
1390
+ ageMs,
1391
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
1392
+ };
1393
+ }
1394
+ async function getLatestMarketBreadth(params) {
1395
+ await prepareMarketContextSchemaForRead("binance");
1396
+ const res = await queryMarketContext(
1397
+ `
1398
+ SELECT
1399
+ universe,
1400
+ interval,
1401
+ ts,
1402
+ symbols_count::int AS "symbolsCount",
1403
+ advancers::int AS advancers,
1404
+ decliners::int AS decliners,
1405
+ unchanged::int AS unchanged,
1406
+ advance_decline_ratio AS "advanceDeclineRatio",
1407
+ pct_above_ma20 AS "pctAboveMa20",
1408
+ pct_above_ma50 AS "pctAboveMa50",
1409
+ equal_weighted_return AS "equalWeightedReturn",
1410
+ volume_weighted_return AS "volumeWeightedReturn",
1411
+ dispersion,
1412
+ btc_return_1h AS "btcReturn1h",
1413
+ btc_return_4h AS "btcReturn4h",
1414
+ btc_return_24h AS "btcReturn24h",
1415
+ alt_basket_return_1h AS "altBasketReturn1h",
1416
+ alt_basket_return_4h AS "altBasketReturn4h",
1417
+ alt_basket_return_24h AS "altBasketReturn24h",
1418
+ btc_vs_alt_return_1h AS "btcVsAltReturn1h",
1419
+ btc_vs_alt_return_4h AS "btcVsAltReturn4h",
1420
+ btc_vs_alt_return_24h AS "btcVsAltReturn24h",
1421
+ btc_turnover_share_1h AS "btcTurnoverShare1h",
1422
+ btc_turnover_share_24h AS "btcTurnoverShare24h",
1423
+ btc_turnover_share_change_24h AS "btcTurnoverShareChange24h",
1424
+ alt_vol_to_btc_vol_24h AS "altVolToBtcVol24h",
1425
+ alt_dispersion_24h AS "altDispersion24h",
1426
+ btc_alt_regime AS "btcAltRegime",
1427
+ source
1428
+ FROM market_breadth
1429
+ WHERE universe = $1
1430
+ AND interval = $2
1431
+ AND ts <= to_timestamp($3/1000.0)
1432
+ ORDER BY ts DESC
1433
+ LIMIT 1
1434
+ `,
1435
+ [params.universe, params.interval, params.atMs],
1436
+ params
1437
+ );
1438
+ const row = res.rows[0];
1439
+ if (!row) return null;
1440
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1441
+ return {
1442
+ ...row,
1443
+ ageMs,
1444
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
1445
+ };
1446
+ }
1447
+ async function getLatestMarketGlobalContext(params) {
1448
+ await prepareMarketContextSchemaForRead("coinmarketcap");
1449
+ const source = params.source ?? "coinmarketcap_global";
1450
+ const res = await queryMarketContext(
1451
+ `
1452
+ SELECT
1453
+ source,
1454
+ ts,
1455
+ updated_at_ts AS "updatedAt",
1456
+ active_cryptocurrencies::int AS "activeCryptocurrencies",
1457
+ active_exchanges::int AS "activeExchanges",
1458
+ active_market_pairs::int AS "activeMarketPairs",
1459
+ markets::int AS markets,
1460
+ total_market_cap_usd AS "totalMarketCapUsd",
1461
+ total_volume_usd AS "totalVolumeUsd",
1462
+ total_volume_reported_usd AS "totalVolumeReportedUsd",
1463
+ btc_dominance_pct AS "btcDominancePct",
1464
+ eth_dominance_pct AS "ethDominancePct",
1465
+ alt_market_cap_usd AS "altMarketCapUsd",
1466
+ alt_volume_usd AS "altVolumeUsd",
1467
+ alt_volume_reported_usd AS "altVolumeReportedUsd",
1468
+ btc_to_alt_market_cap_ratio AS "btcToAltMarketCapRatio",
1469
+ market_cap_change_pct_24h_usd AS "marketCapChangePct24hUsd"
1470
+ FROM market_global_context
1471
+ WHERE source = $1
1472
+ AND ts + CASE
1473
+ WHEN source = 'coinmarketcap_global' THEN interval '1 day'
1474
+ ELSE interval '0 seconds'
1475
+ END <= to_timestamp($2/1000.0)
1476
+ ORDER BY ts DESC
1477
+ LIMIT 1
1478
+ `,
1479
+ [source, params.atMs],
1480
+ params
1481
+ );
1482
+ const row = res.rows[0];
1483
+ if (!row) return null;
1484
+ const previousRes = await queryMarketContext(
1485
+ `
1486
+ SELECT
1487
+ btc_dominance_pct AS "btcDominancePct",
1488
+ eth_dominance_pct AS "ethDominancePct",
1489
+ alt_market_cap_usd AS "altMarketCapUsd",
1490
+ alt_volume_usd AS "altVolumeUsd"
1491
+ FROM market_global_context
1492
+ WHERE source = $1
1493
+ AND ts <= $2::timestamptz - interval '24 hours'
1494
+ ORDER BY ts DESC
1495
+ LIMIT 1
1496
+ `,
1497
+ [source, row.ts],
1498
+ params
1499
+ );
1500
+ const previousDominance = previousRes.rows[0]?.btcDominancePct == null ? null : Number(previousRes.rows[0].btcDominancePct);
1501
+ const previousEthDominance = previousRes.rows[0]?.ethDominancePct == null ? null : Number(previousRes.rows[0].ethDominancePct);
1502
+ const previousAltMarketCap = previousRes.rows[0]?.altMarketCapUsd == null ? null : Number(previousRes.rows[0].altMarketCapUsd);
1503
+ const previousAltVolume = previousRes.rows[0]?.altVolumeUsd == null ? null : Number(previousRes.rows[0].altVolumeUsd);
1504
+ const currentDominance = row.btcDominancePct == null ? null : Number(row.btcDominancePct);
1505
+ const currentEthDominance = row.ethDominancePct == null ? null : Number(row.ethDominancePct);
1506
+ const currentAltMarketCap = row.altMarketCapUsd == null ? null : Number(row.altMarketCapUsd);
1507
+ const currentAltVolume = row.altVolumeUsd == null ? null : Number(row.altVolumeUsd);
1508
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1509
+ return {
1510
+ ...row,
1511
+ ageMs,
1512
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1513
+ btcDominanceChange24hPct: currentDominance != null && previousDominance != null ? currentDominance - previousDominance : null,
1514
+ ethDominanceChange24hPct: currentEthDominance != null && previousEthDominance != null ? currentEthDominance - previousEthDominance : null,
1515
+ altMarketCapChange24hPct: currentAltMarketCap != null && previousAltMarketCap != null && previousAltMarketCap > 0 ? (currentAltMarketCap - previousAltMarketCap) / previousAltMarketCap : null,
1516
+ altVolumeChange24hPct: currentAltVolume != null && previousAltVolume != null && previousAltVolume > 0 ? (currentAltVolume - previousAltVolume) / previousAltVolume : null
1517
+ };
1518
+ }
1519
+ async function getMarketGlobalContextCoverage(params) {
1520
+ await ensureBinanceMarketSchema();
1521
+ const pool = getPool();
1522
+ const res = await pool.query(
1523
+ `
1524
+ SELECT
1525
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1526
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1527
+ COUNT(*)::int AS rows
1528
+ FROM market_global_context
1529
+ WHERE source = $1
1530
+ AND ts >= to_timestamp($2/1000.0)
1531
+ AND ts <= to_timestamp($3/1000.0)
1532
+ `,
1533
+ [params.source, params.startMs, params.endMs]
1534
+ );
1535
+ const row = res.rows[0];
1536
+ const rows = Number(row?.rows ?? 0);
1537
+ const firstMs = Number(row?.first_ms);
1538
+ const lastMs = Number(row?.last_ms);
1539
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
1540
+ return null;
1541
+ }
1542
+ return { firstMs, lastMs, rows };
1543
+ }
1544
+ async function getMarketReferenceAssetContextCoverage(params) {
1545
+ const symbols = [
1546
+ ...new Set(
1547
+ params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
1548
+ )
1549
+ ];
1550
+ const coverage = /* @__PURE__ */ new Map();
1551
+ if (!symbols.length) return coverage;
1552
+ await ensureBinanceMarketSchema();
1553
+ const pool = getPool();
1554
+ const res = await pool.query(
1555
+ `
1556
+ SELECT
1557
+ symbol,
1558
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1559
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1560
+ COUNT(*)::int AS rows
1561
+ FROM market_reference_asset_context
1562
+ WHERE source = $1
1563
+ AND symbol = ANY($2)
1564
+ AND interval = $3
1565
+ AND ts >= to_timestamp($4/1000.0)
1566
+ AND ts <= to_timestamp($5/1000.0)
1567
+ GROUP BY symbol
1568
+ `,
1569
+ [params.source, symbols, params.interval, params.startMs, params.endMs]
1570
+ );
1571
+ for (const row of res.rows) {
1572
+ const firstMs = Number(row.first_ms);
1573
+ const lastMs = Number(row.last_ms);
1574
+ const rows = Number(row.rows);
1575
+ if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
1576
+ coverage.set(row.symbol.toUpperCase(), { firstMs, lastMs, rows });
1577
+ }
1578
+ }
1579
+ return coverage;
1580
+ }
1581
+ async function getLatestMarketReferenceAssetContexts(params) {
1582
+ const source = params.source ?? "coinmarketcap_reference_asset";
1583
+ const interval = params.interval ?? "1d";
1584
+ const symbols = [
1585
+ ...new Set(
1586
+ params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
1587
+ )
1588
+ ];
1589
+ const rows = /* @__PURE__ */ new Map();
1590
+ if (!symbols.length) return rows;
1591
+ await prepareMarketContextSchemaForRead("coinmarketcap");
1592
+ const res = await queryMarketContext(
1593
+ `
1594
+ SELECT DISTINCT ON (symbol)
1595
+ source,
1596
+ symbol,
1597
+ cmc_id AS "cmcId",
1598
+ interval,
1599
+ ts,
1600
+ open_usd AS "openUsd",
1601
+ high_usd AS "highUsd",
1602
+ low_usd AS "lowUsd",
1603
+ close_usd AS "closeUsd",
1604
+ volume_usd AS "volumeUsd",
1605
+ market_cap_usd AS "marketCapUsd"
1606
+ FROM market_reference_asset_context
1607
+ WHERE source = $1
1608
+ AND symbol = ANY($2)
1609
+ AND interval = $3
1610
+ AND ts + CASE interval
1611
+ WHEN '1d' THEN interval '1 day'
1612
+ WHEN '1h' THEN interval '1 hour'
1613
+ ELSE interval '0 seconds'
1614
+ END <= to_timestamp($4/1000.0)
1615
+ ORDER BY symbol ASC, ts DESC
1616
+ `,
1617
+ [source, symbols, interval, params.atMs],
1618
+ params
1619
+ );
1620
+ for (const row of res.rows) {
1621
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1622
+ rows.set(row.symbol.toUpperCase(), {
1623
+ ...row,
1624
+ ageMs,
1625
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
1626
+ });
1627
+ }
1628
+ return rows;
1629
+ }
1630
+ async function getLatestMarketCmcExchangeLiquidityContext(params) {
1631
+ await prepareMarketContextSchemaForRead("coinmarketcap");
1632
+ const source = params.source ?? "coinmarketcap_exchange_liquidity";
1633
+ const interval = params.interval ?? "1d";
1634
+ const res = await queryMarketContext(
1635
+ `
1636
+ SELECT
1637
+ source,
1638
+ interval,
1639
+ ts,
1640
+ exchanges_count::int AS "exchangesCount",
1641
+ total_volume_usd AS "totalVolumeUsd",
1642
+ binance_volume_usd AS "binanceVolumeUsd",
1643
+ binance_volume_share AS "binanceVolumeShare",
1644
+ top_exchange_volume_share AS "topExchangeVolumeShare",
1645
+ liquidity_regime AS "liquidityRegime"
1646
+ FROM market_cmc_exchange_liquidity_context
1647
+ WHERE source = $1
1648
+ AND interval = $2
1649
+ AND ts + CASE interval
1650
+ WHEN '1d' THEN interval '1 day'
1651
+ WHEN '1h' THEN interval '1 hour'
1652
+ ELSE interval '0 seconds'
1653
+ END <= to_timestamp($3/1000.0)
1654
+ ORDER BY ts DESC
1655
+ LIMIT 1
1656
+ `,
1657
+ [source, interval, params.atMs],
1658
+ params
1659
+ );
1660
+ const row = res.rows[0];
1661
+ if (!row) return null;
1662
+ const previousRes = await queryMarketContext(
1663
+ `
1664
+ SELECT total_volume_usd AS "totalVolumeUsd"
1665
+ FROM market_cmc_exchange_liquidity_context
1666
+ WHERE source = $1
1667
+ AND interval = $2
1668
+ AND ts <= $3::timestamptz - interval '24 hours'
1669
+ ORDER BY ts DESC
1670
+ LIMIT 1
1671
+ `,
1672
+ [source, interval, row.ts],
1673
+ params
1674
+ );
1675
+ const currentTotal = row.totalVolumeUsd == null ? null : Number(row.totalVolumeUsd);
1676
+ const previousTotal = previousRes.rows[0]?.totalVolumeUsd == null ? null : Number(previousRes.rows[0].totalVolumeUsd);
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
+ totalVolumeChange24hPct: currentTotal != null && previousTotal != null && previousTotal > 0 ? (currentTotal - previousTotal) / previousTotal : null
1683
+ };
1684
+ }
1685
+ async function getLatestMarketCmcIndexContexts(params) {
1686
+ const source = params.source ?? "coinmarketcap_index";
1687
+ const interval = params.interval ?? "1d";
1688
+ const indexSlugs = [
1689
+ ...new Set(
1690
+ params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
1691
+ (slug) => ["cmc100", "cmc20"].includes(slug)
1692
+ )
1693
+ )
1694
+ ];
1695
+ const rows = /* @__PURE__ */ new Map();
1696
+ if (!indexSlugs.length) return rows;
1697
+ await prepareMarketContextSchemaForRead("coinmarketcap");
1698
+ const res = await queryMarketContext(
1699
+ `
1700
+ SELECT DISTINCT ON (index_slug)
1701
+ source,
1702
+ index_slug AS "indexSlug",
1703
+ interval,
1704
+ ts,
1705
+ value,
1706
+ constituents_count::int AS "constituentsCount",
1707
+ top_constituent_symbol AS "topConstituentSymbol",
1708
+ top_constituent_weight_pct AS "topConstituentWeightPct",
1709
+ constituents
1710
+ FROM market_cmc_index_context
1711
+ WHERE source = $1
1712
+ AND index_slug = ANY($2)
1713
+ AND interval = $3
1714
+ AND ts + CASE interval
1715
+ WHEN '1d' THEN interval '1 day'
1716
+ WHEN '1h' THEN interval '1 hour'
1717
+ ELSE interval '0 seconds'
1718
+ END <= to_timestamp($4/1000.0)
1719
+ ORDER BY index_slug ASC, ts DESC
1720
+ `,
1721
+ [source, indexSlugs, interval, params.atMs],
1722
+ params
1723
+ );
1724
+ for (const row of res.rows) {
1725
+ const previousRes = await queryMarketContext(
1726
+ `
1727
+ SELECT value
1728
+ FROM market_cmc_index_context
1729
+ WHERE source = $1
1730
+ AND index_slug = $2
1731
+ AND interval = $3
1732
+ AND ts <= $4::timestamptz - interval '24 hours'
1733
+ ORDER BY ts DESC
1734
+ LIMIT 1
1735
+ `,
1736
+ [source, row.indexSlug, interval, row.ts],
1737
+ params
1738
+ );
1739
+ const currentValue = row.value == null ? null : Number(row.value);
1740
+ const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
1741
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1742
+ rows.set(row.indexSlug, {
1743
+ ...row,
1744
+ ageMs,
1745
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1746
+ valueChange24hPct: currentValue != null && previousValue != null && previousValue > 0 ? (currentValue - previousValue) / previousValue : null
1747
+ });
1748
+ }
1749
+ return rows;
1750
+ }
1751
+ async function getLatestMarketCmcFearGreedContext(params) {
1752
+ await prepareMarketContextSchemaForRead("coinmarketcap");
1753
+ const source = params.source ?? "coinmarketcap_fear_greed";
1754
+ const interval = params.interval ?? "1d";
1755
+ const res = await queryMarketContext(
1756
+ `
1757
+ SELECT
1758
+ source,
1759
+ interval,
1760
+ ts,
1761
+ value::int AS value,
1762
+ classification,
1763
+ sentiment_regime AS "sentimentRegime"
1764
+ FROM market_cmc_fear_greed_context
1765
+ WHERE source = $1
1766
+ AND interval = $2
1767
+ AND ts + CASE interval
1768
+ WHEN '1d' THEN interval '1 day'
1769
+ WHEN '1h' THEN interval '1 hour'
1770
+ ELSE interval '0 seconds'
1771
+ END <= to_timestamp($3/1000.0)
1772
+ ORDER BY ts DESC
1773
+ LIMIT 1
1774
+ `,
1775
+ [source, interval, params.atMs],
1776
+ params
1777
+ );
1778
+ const row = res.rows[0];
1779
+ if (!row) return null;
1780
+ const previousRes = await queryMarketContext(
1781
+ `
1782
+ SELECT
1783
+ value::int AS value,
1784
+ '24h' AS bucket
1785
+ FROM market_cmc_fear_greed_context
1786
+ WHERE source = $1
1787
+ AND interval = $2
1788
+ AND ts <= $3::timestamptz - interval '24 hours'
1789
+ ORDER BY ts DESC
1790
+ LIMIT 1
1791
+ `,
1792
+ [source, interval, row.ts],
1793
+ params
1794
+ );
1795
+ const previous7dRes = await queryMarketContext(
1796
+ `
1797
+ SELECT value::int AS value
1798
+ FROM market_cmc_fear_greed_context
1799
+ WHERE source = $1
1800
+ AND interval = $2
1801
+ AND ts <= $3::timestamptz - interval '7 days'
1802
+ ORDER BY ts DESC
1803
+ LIMIT 1
1804
+ `,
1805
+ [source, interval, row.ts],
1806
+ params
1807
+ );
1808
+ const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
1809
+ const previous7dValue = previous7dRes.rows[0]?.value == null ? null : Number(previous7dRes.rows[0].value);
1810
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
1811
+ return {
1812
+ ...row,
1813
+ ageMs,
1814
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
1815
+ valueChange24h: previousValue == null ? null : row.value - previousValue,
1816
+ valueChange7d: previous7dValue == null ? null : row.value - previous7dValue
1817
+ };
1818
+ }
1819
+ async function getMarketCmcFearGreedContextCoverage(params) {
1820
+ await ensureBinanceMarketSchema();
1821
+ const pool = getPool();
1822
+ const res = await pool.query(
1823
+ `
1824
+ SELECT
1825
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1826
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1827
+ COUNT(*)::int AS rows
1828
+ FROM market_cmc_fear_greed_context
1829
+ WHERE source = $1
1830
+ AND interval = $2
1831
+ AND ts >= to_timestamp($3/1000.0)
1832
+ AND ts <= to_timestamp($4/1000.0)
1833
+ `,
1834
+ [params.source, params.interval, params.startMs, params.endMs]
1835
+ );
1836
+ const rows = Number(res.rows[0]?.rows ?? 0);
1837
+ const firstMs = Number(res.rows[0]?.first_ms);
1838
+ const lastMs = Number(res.rows[0]?.last_ms);
1839
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
1840
+ return null;
1841
+ }
1842
+ return { firstMs, lastMs, rows };
1843
+ }
1844
+ async function getMarketCmcExchangeLiquidityContextCoverage(params) {
1845
+ await ensureBinanceMarketSchema();
1846
+ const pool = getPool();
1847
+ const res = await pool.query(
1848
+ `
1849
+ SELECT
1850
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1851
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1852
+ COUNT(*)::int AS rows
1853
+ FROM market_cmc_exchange_liquidity_context
1854
+ WHERE source = $1
1855
+ AND interval = $2
1856
+ AND ts >= to_timestamp($3/1000.0)
1857
+ AND ts <= to_timestamp($4/1000.0)
1858
+ `,
1859
+ [params.source, params.interval, params.startMs, params.endMs]
1860
+ );
1861
+ const rows = Number(res.rows[0]?.rows ?? 0);
1862
+ const firstMs = Number(res.rows[0]?.first_ms);
1863
+ const lastMs = Number(res.rows[0]?.last_ms);
1864
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
1865
+ return null;
1866
+ }
1867
+ return { firstMs, lastMs, rows };
1868
+ }
1869
+ async function getMarketCmcIndexContextCoverage(params) {
1870
+ const indexSlugs = [
1871
+ ...new Set(
1872
+ params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
1873
+ (slug) => ["cmc100", "cmc20"].includes(slug)
1874
+ )
1875
+ )
1876
+ ];
1877
+ const coverage = /* @__PURE__ */ new Map();
1878
+ if (!indexSlugs.length) return coverage;
1879
+ await ensureBinanceMarketSchema();
1880
+ const pool = getPool();
1881
+ const res = await pool.query(
1882
+ `
1883
+ SELECT
1884
+ index_slug,
1885
+ extract(epoch from MIN(ts))*1000 AS first_ms,
1886
+ extract(epoch from MAX(ts))*1000 AS last_ms,
1887
+ COUNT(*)::int AS rows
1888
+ FROM market_cmc_index_context
1889
+ WHERE source = $1
1890
+ AND index_slug = ANY($2)
1891
+ AND interval = $3
1892
+ AND ts >= to_timestamp($4/1000.0)
1893
+ AND ts <= to_timestamp($5/1000.0)
1894
+ GROUP BY index_slug
1895
+ `,
1896
+ [params.source, indexSlugs, params.interval, params.startMs, params.endMs]
1897
+ );
1898
+ for (const row of res.rows) {
1899
+ const indexSlug = row.index_slug;
1900
+ const firstMs = Number(row.first_ms);
1901
+ const lastMs = Number(row.last_ms);
1902
+ const rows = Number(row.rows);
1903
+ if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
1904
+ coverage.set(indexSlug, { firstMs, lastMs, rows });
1905
+ }
1906
+ }
1907
+ return coverage;
1908
+ }
1909
+ async function getMarketTradeFlowCoverage(params) {
1910
+ const symbols = [
1911
+ ...new Set(params.symbols.map((item) => item.toUpperCase()))
1912
+ ];
1913
+ if (!symbols.length) return /* @__PURE__ */ new Map();
1914
+ await ensureBinanceMarketSchema();
1915
+ const pool = getPool();
1916
+ const res = await pool.query(
1917
+ `
1918
+ SELECT
1919
+ symbol,
1920
+ MIN(ts) AS first_ts,
1921
+ MAX(ts) AS last_ts,
1922
+ COUNT(*)::int AS rows
1923
+ FROM market_trade_flow
1924
+ WHERE symbol = ANY($1)
1925
+ AND interval = $2
1926
+ AND ts >= to_timestamp($3/1000.0)
1927
+ AND ts <= to_timestamp($4/1000.0)
1928
+ GROUP BY symbol
1929
+ `,
1930
+ [symbols, params.interval, params.startMs, params.endMs]
1931
+ );
1932
+ return new Map(
1933
+ res.rows.map((row) => [
1934
+ String(row.symbol).toUpperCase(),
1935
+ {
1936
+ firstMs: new Date(row.first_ts).getTime(),
1937
+ lastMs: new Date(row.last_ts).getTime(),
1938
+ rows: Number(row.rows) || 0
1939
+ }
1940
+ ])
1941
+ );
1942
+ }
1943
+ var getTableRowCountIfExists = async (tableName) => {
1944
+ const pool = getPool();
1945
+ const exists = await pool.query("SELECT to_regclass($1) AS name", [
1946
+ tableName
1947
+ ]);
1948
+ if (!exists.rows[0]?.name) return null;
1949
+ const count = await pool.query(
1950
+ `SELECT COUNT(*)::int AS rows FROM ${tableName}`
1951
+ );
1952
+ return Number(count.rows[0]?.rows ?? 0);
1953
+ };
1954
+ async function cleanupDeprecatedMarketContext(params = {}) {
1955
+ const apply = Boolean(params.apply);
1956
+ const pool = getPool();
1957
+ const items = [];
1958
+ const cleanupRows = async ({
1959
+ tableName,
1960
+ whereSql,
1961
+ name
1962
+ }) => {
1963
+ const tableRows = await getTableRowCountIfExists(tableName);
1964
+ if (tableRows == null) return;
1965
+ const count = await pool.query(
1966
+ `
1967
+ SELECT COUNT(*)::int AS rows
1968
+ FROM ${tableName}
1969
+ WHERE ${whereSql}
1970
+ `
1971
+ );
1972
+ const rows = Number(count.rows[0]?.rows ?? 0);
1973
+ if (rows <= 0) return;
1974
+ if (apply) {
1975
+ await pool.query(
1976
+ `
1977
+ DELETE FROM ${tableName}
1978
+ WHERE ${whereSql}
1979
+ `
1980
+ );
1981
+ }
1982
+ items.push({
1983
+ kind: "rows",
1984
+ name,
1985
+ rows,
1986
+ action: "delete_rows",
1987
+ applied: apply
1988
+ });
1989
+ };
1990
+ for (const tableName of ["market_order_book_depth", "onchain_flow_context"]) {
1991
+ const rows = await getTableRowCountIfExists(tableName);
1992
+ if (rows == null) continue;
1993
+ if (apply) {
1994
+ await pool.query(`DROP TABLE IF EXISTS ${tableName}`);
1995
+ }
1996
+ items.push({
1997
+ kind: "table",
1998
+ name: tableName,
1999
+ rows,
2000
+ action: "drop_table",
2001
+ applied: apply
2002
+ });
2003
+ }
2004
+ await cleanupRows({
2005
+ tableName: "market_global_context",
2006
+ whereSql: "source = 'coingecko_global'",
2007
+ name: "market_global_context/source=coingecko_global"
2008
+ });
2009
+ await cleanupRows({
2010
+ tableName: "market_global_context",
2011
+ whereSql: "source = 'coinmarketcap_global_hourly'",
2012
+ name: "market_global_context/source=coinmarketcap_global_hourly"
2013
+ });
2014
+ await cleanupRows({
2015
+ tableName: "market_reference_asset_context",
2016
+ whereSql: "source = 'coinmarketcap_reference_asset' AND interval = '1h'",
2017
+ name: "market_reference_asset_context/source=coinmarketcap_reference_asset/interval=1h"
2018
+ });
2019
+ await cleanupRows({
2020
+ tableName: "market_cmc_breadth_context",
2021
+ whereSql: "source = 'coinmarketcap_market_breadth'",
2022
+ name: "market_cmc_breadth_context/source=coinmarketcap_market_breadth"
2023
+ });
2024
+ await cleanupRows({
2025
+ tableName: "market_context_backfill_coverage",
2026
+ whereSql: "(source IN ('coinmarketcap_global_hourly', 'coinmarketcap_market_breadth') OR (source = 'coinmarketcap_reference_asset' AND interval = '1h'))",
2027
+ name: "market_context_backfill_coverage/deprecated_cmc_sources"
2028
+ });
2029
+ return items;
2030
+ }
2031
+ async function getMarketBreadthCoverage(params) {
2032
+ await ensureBinanceMarketSchema();
2033
+ const pool = getPool();
2034
+ const res = await pool.query(
2035
+ `
2036
+ SELECT
2037
+ MIN(ts) AS first_ts,
2038
+ MAX(ts) AS last_ts,
2039
+ COUNT(*)::int AS rows,
2040
+ COUNT(*) FILTER (
2041
+ WHERE btc_alt_regime IS NOT NULL
2042
+ AND btc_return_24h IS NOT NULL
2043
+ AND alt_basket_return_24h IS NOT NULL
2044
+ )::int AS btc_alt_metrics_rows
2045
+ FROM market_breadth
2046
+ WHERE universe = $1
2047
+ AND interval = $2
2048
+ AND ts >= to_timestamp($3/1000.0)
2049
+ AND ts <= to_timestamp($4/1000.0)
2050
+ `,
2051
+ [params.universe, params.interval, params.startMs, params.endMs]
2052
+ );
2053
+ const row = res.rows[0];
2054
+ if (!row?.first_ts || !row?.last_ts) return null;
2055
+ return {
2056
+ firstMs: new Date(row.first_ts).getTime(),
2057
+ lastMs: new Date(row.last_ts).getTime(),
2058
+ rows: Number(row.rows) || 0,
2059
+ btcAltMetricsRows: Number(row.btc_alt_metrics_rows) || 0
2060
+ };
2061
+ }
2062
+ // Annotate the CommonJS export names for ESM import in node:
2063
+ 0 && (module.exports = {
2064
+ cleanupDeprecatedMarketContext,
2065
+ ensureBinanceMarketSchema,
2066
+ ensureCoinMarketCapContextSchema,
2067
+ ensureMarketContextSchemas,
2068
+ getLatestMarketBreadth,
2069
+ getLatestMarketCmcExchangeLiquidityContext,
2070
+ getLatestMarketCmcFearGreedContext,
2071
+ getLatestMarketCmcIndexContexts,
2072
+ getLatestMarketGlobalContext,
2073
+ getLatestMarketReferenceAssetContexts,
2074
+ getLatestMarketTradeFlow,
2075
+ getMarketBreadthCoverage,
2076
+ getMarketCmcExchangeLiquidityContextCoverage,
2077
+ getMarketCmcFearGreedContextCoverage,
2078
+ getMarketCmcIndexContextCoverage,
2079
+ getMarketContextBackfillCoverage,
2080
+ getMarketGlobalContextCoverage,
2081
+ getMarketReferenceAssetContextCoverage,
2082
+ getMarketTradeFlowCoverage,
2083
+ upsertMarketBreadthRows,
2084
+ upsertMarketCmcExchangeLiquidityContextRows,
2085
+ upsertMarketCmcFearGreedContextRows,
2086
+ upsertMarketCmcIndexContextRows,
2087
+ upsertMarketContextBackfillCoverage,
2088
+ upsertMarketGlobalContextRows,
2089
+ upsertMarketReferenceAssetContextRows,
2090
+ upsertMarketTradeFlowRows
2091
+ });