@tradejs/infra 2.0.17 → 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
package/dist/timescale.js CHANGED
@@ -84,25 +84,21 @@ __export(timescale_exports, {
84
84
  waitForDbReady: () => waitForDbReady
85
85
  });
86
86
  module.exports = __toCommonJS(timescale_exports);
87
+
88
+ // src/timescale/pool.ts
87
89
  var import_pg = require("pg");
88
- var import_types = require("@tradejs/types");
89
90
  var getPool = () => {
90
91
  if (!global.__pgPool__) {
91
- const host = process.env.PG_HOST || "127.0.0.1";
92
- const port = Number(process.env.PG_PORT ?? 5432);
93
- const user = process.env.PG_USER || "app";
94
- const password = String(process.env.PG_PASSWORD ?? "app");
95
- const database = process.env.PG_DATABASE || process.env.PG_DB || "app";
96
92
  const max = Number(process.env.PG_POOL_MAX ?? 10);
97
93
  const connectionTimeoutMillis = Number(
98
94
  process.env.PG_CONNECTION_TIMEOUT_MS ?? 3e4
99
95
  );
100
96
  global.__pgPool__ = new import_pg.Pool({
101
- host,
102
- port,
103
- user,
104
- password,
105
- database,
97
+ host: process.env.PG_HOST || "127.0.0.1",
98
+ port: Number(process.env.PG_PORT ?? 5432),
99
+ user: process.env.PG_USER || "app",
100
+ password: String(process.env.PG_PASSWORD ?? "app"),
101
+ database: process.env.PG_DATABASE || process.env.PG_DB || "app",
106
102
  max: Number.isFinite(max) && max > 0 ? Math.floor(max) : 10,
107
103
  idleTimeoutMillis: 3e4,
108
104
  connectionTimeoutMillis: Number.isFinite(connectionTimeoutMillis) && connectionTimeoutMillis > 0 ? Math.floor(connectionTimeoutMillis) : 3e4
@@ -110,63 +106,29 @@ var getPool = () => {
110
106
  }
111
107
  return global.__pgPool__;
112
108
  };
113
- var candlesSchemaReady = false;
114
- var derivativesSchemaReady = false;
115
- var spreadSchemaReady = false;
116
- var binanceMarketSchemaReady = false;
117
- var hyperliquidWhaleSchemaReady = false;
118
- var candlesSchemaReadyPromise = null;
119
- var derivativesSchemaReadyPromise = null;
120
- var spreadSchemaReadyPromise = null;
121
- var binanceMarketSchemaReadyPromise = null;
122
- var hyperliquidWhaleSchemaReadyPromise = null;
123
- var marketContextSchemaMode = "ensure";
124
- var verifiedMarketContextSchemas = /* @__PURE__ */ new Set();
125
- var configureTimescaleMarketContextSchemaMode = (mode) => {
126
- marketContextSchemaMode = mode;
127
- verifiedMarketContextSchemas.clear();
128
- };
129
- var closeTimescalePool = async () => {
109
+ var closePool = async () => {
130
110
  const pool = global.__pgPool__;
131
- if (!pool) {
132
- return;
133
- }
111
+ if (!pool) return;
134
112
  global.__pgPool__ = void 0;
135
- candlesSchemaReady = false;
136
- derivativesSchemaReady = false;
137
- spreadSchemaReady = false;
138
- binanceMarketSchemaReady = false;
139
- hyperliquidWhaleSchemaReady = false;
140
- candlesSchemaReadyPromise = null;
141
- derivativesSchemaReadyPromise = null;
142
- spreadSchemaReadyPromise = null;
143
- binanceMarketSchemaReadyPromise = null;
144
- hyperliquidWhaleSchemaReadyPromise = null;
145
- verifiedMarketContextSchemas.clear();
146
113
  await pool.end();
147
114
  };
148
- var CANDLES_SCHEMA_LOCK_KEY = 61e4;
149
- var DERIVATIVES_SCHEMA_LOCK_KEY = 610001;
150
- var SPREAD_SCHEMA_LOCK_KEY = 610002;
151
- var BINANCE_MARKET_SCHEMA_LOCK_KEY = 610003;
152
- var HYPERLIQUID_WHALE_SCHEMA_LOCK_KEY = 610004;
153
- var PG_SAFE_MAX_BIND_PARAMS = 3e4;
154
- var resolveMarketContextQueryTimeoutMs = (override) => {
115
+
116
+ // src/timescale/query.ts
117
+ var resolveQueryTimeoutMs = (override) => {
155
118
  if (Number.isFinite(override) && Number(override) > 0) {
156
119
  return Math.floor(Number(override));
157
120
  }
158
121
  const configured = Number(process.env.MARKET_CONTEXT_SQL_TIMEOUT_MS);
159
122
  return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : 3e4;
160
123
  };
161
- var createMarketContextQueryError = (name, message) => {
124
+ var createQueryError = (name, message) => {
162
125
  const error = new Error(message);
163
126
  error.name = name;
164
127
  return error;
165
128
  };
166
129
  var queryMarketContext = async (text, values, options = {}) => {
167
- const pool = getPool();
168
- const client = await pool.connect();
169
- const timeoutMs = resolveMarketContextQueryTimeoutMs(options.timeoutMs);
130
+ const client = await getPool().connect();
131
+ const timeoutMs = resolveQueryTimeoutMs(options.timeoutMs);
170
132
  let released = false;
171
133
  let rejectCancellation;
172
134
  const release = (error) => {
@@ -182,14 +144,11 @@ var queryMarketContext = async (text, values, options = {}) => {
182
144
  rejectCancellation?.(error);
183
145
  };
184
146
  const onAbort = () => cancel(
185
- createMarketContextQueryError(
186
- "AbortError",
187
- "Timescale market-context query aborted"
188
- )
147
+ createQueryError("AbortError", "Timescale market-context query aborted")
189
148
  );
190
149
  const timer = setTimeout(
191
150
  () => cancel(
192
- createMarketContextQueryError(
151
+ createQueryError(
193
152
  "TimescaleQueryTimeoutError",
194
153
  `Timescale market-context query exceeded ${timeoutMs}ms`
195
154
  )
@@ -200,24 +159,67 @@ var queryMarketContext = async (text, values, options = {}) => {
200
159
  options.signal?.addEventListener("abort", onAbort, { once: true });
201
160
  try {
202
161
  if (options.signal?.aborted) {
203
- const error = createMarketContextQueryError(
162
+ const error = createQueryError(
204
163
  "AbortError",
205
164
  "Timescale market-context query aborted"
206
165
  );
207
166
  release(error);
208
167
  throw error;
209
168
  }
210
- const query = client.query(text, values);
211
- return await Promise.race([query, cancellation]);
169
+ return await Promise.race([client.query(text, values), cancellation]);
212
170
  } finally {
213
171
  clearTimeout(timer);
214
172
  options.signal?.removeEventListener("abort", onAbort);
215
173
  release();
216
174
  }
217
175
  };
176
+
177
+ // src/timescale/values.ts
178
+ var PG_SAFE_MAX_BIND_PARAMS = 3e4;
218
179
  var normalizeCandleProvider = (provider) => String(provider || "").trim().toLowerCase();
219
180
  var normalizeCandleSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
220
181
  var getSafeBulkInsertRows = (columnsCount) => Math.max(1, Math.floor(PG_SAFE_MAX_BIND_PARAMS / columnsCount));
182
+ var toMarketFeatureAge = (rowTs, atMs) => {
183
+ const ageMs = atMs - rowTs.getTime();
184
+ return Number.isFinite(ageMs) ? ageMs : null;
185
+ };
186
+
187
+ // src/timescale/internal.ts
188
+ var candlesSchemaReady = false;
189
+ var derivativesSchemaReady = false;
190
+ var spreadSchemaReady = false;
191
+ var binanceMarketSchemaReady = false;
192
+ var hyperliquidWhaleSchemaReady = false;
193
+ var candlesSchemaReadyPromise = null;
194
+ var derivativesSchemaReadyPromise = null;
195
+ var spreadSchemaReadyPromise = null;
196
+ var binanceMarketSchemaReadyPromise = null;
197
+ var hyperliquidWhaleSchemaReadyPromise = null;
198
+ var marketContextSchemaMode = "ensure";
199
+ var verifiedMarketContextSchemas = /* @__PURE__ */ new Set();
200
+ var configureTimescaleMarketContextSchemaMode = (mode) => {
201
+ marketContextSchemaMode = mode;
202
+ verifiedMarketContextSchemas.clear();
203
+ };
204
+ var closeTimescalePool = async () => {
205
+ candlesSchemaReady = false;
206
+ derivativesSchemaReady = false;
207
+ spreadSchemaReady = false;
208
+ binanceMarketSchemaReady = false;
209
+ hyperliquidWhaleSchemaReady = false;
210
+ candlesSchemaReadyPromise = null;
211
+ derivativesSchemaReadyPromise = null;
212
+ spreadSchemaReadyPromise = null;
213
+ binanceMarketSchemaReadyPromise = null;
214
+ hyperliquidWhaleSchemaReadyPromise = null;
215
+ verifiedMarketContextSchemas.clear();
216
+ await closePool();
217
+ };
218
+ var CANDLES_SCHEMA_LOCK_KEY = 61e4;
219
+ var DERIVATIVES_SCHEMA_LOCK_KEY = 610001;
220
+ var SPREAD_SCHEMA_LOCK_KEY = 610002;
221
+ var BINANCE_MARKET_SCHEMA_LOCK_KEY = 610003;
222
+ var HYPERLIQUID_WHALE_SCHEMA_LOCK_KEY = 610004;
221
223
  var withSchemaLock = async (lockKey, work) => {
222
224
  const pool = getPool();
223
225
  await pool.query("SELECT pg_advisory_lock($1)", [lockKey]);
@@ -283,103 +285,6 @@ var ensureCandlesSchema = async () => {
283
285
  });
284
286
  await candlesSchemaReadyPromise;
285
287
  };
286
- var toRows = (provider, symbol, interval, data) => {
287
- const normalizedProvider = normalizeCandleProvider(provider);
288
- if (!normalizedProvider) {
289
- throw new Error("Candle provider is required");
290
- }
291
- const normalizedSymbol = normalizeCandleSymbol(symbol);
292
- return data.map((i) => ({
293
- provider: normalizedProvider,
294
- symbol: normalizedSymbol,
295
- interval,
296
- ts: new Date(i.timestamp),
297
- // ms -> Date
298
- open: i.open,
299
- high: i.high,
300
- low: i.low,
301
- close: i.close,
302
- volume: i.volume ?? null,
303
- turnover: i.turnover ?? null,
304
- takerBuyBaseVolume: i.takerBuyBaseVolume ?? null,
305
- takerBuyQuoteVolume: i.takerBuyQuoteVolume ?? null,
306
- takerSellBaseVolume: i.takerSellBaseVolume ?? null,
307
- takerSellQuoteVolume: i.takerSellQuoteVolume ?? null
308
- }));
309
- };
310
- async function upsertCandles(rows) {
311
- if (!rows.length) return;
312
- await ensureCandlesSchema();
313
- const pool = getPool();
314
- const cols = [
315
- "provider",
316
- "symbol",
317
- "interval",
318
- "ts",
319
- "open",
320
- "high",
321
- "low",
322
- "close",
323
- "volume",
324
- "turnover",
325
- "taker_buy_base_volume",
326
- "taker_buy_quote_volume",
327
- "taker_sell_base_volume",
328
- "taker_sell_quote_volume"
329
- ];
330
- const maxRows = Math.floor(65535 / cols.length);
331
- if (rows.length > maxRows) {
332
- for (let i = 0; i < rows.length; i += maxRows) {
333
- await upsertCandles(rows.slice(i, i + maxRows));
334
- }
335
- return;
336
- }
337
- const valuesSql = rows.map(
338
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
339
- ).join(",");
340
- const flat = rows.flatMap((r) => [
341
- normalizeCandleProvider(r.provider),
342
- normalizeCandleSymbol(r.symbol),
343
- r.interval,
344
- r.ts,
345
- r.open,
346
- r.high,
347
- r.low,
348
- r.close,
349
- r.volume ?? null,
350
- r.turnover ?? null,
351
- r.takerBuyBaseVolume ?? null,
352
- r.takerBuyQuoteVolume ?? null,
353
- r.takerSellBaseVolume ?? null,
354
- r.takerSellQuoteVolume ?? null
355
- ]);
356
- const sql = `
357
- INSERT INTO candles (${cols.join(",")})
358
- VALUES ${valuesSql}
359
- ON CONFLICT (provider, symbol, interval, ts) DO UPDATE SET
360
- open = EXCLUDED.open,
361
- high = EXCLUDED.high,
362
- low = EXCLUDED.low,
363
- close = EXCLUDED.close,
364
- volume = COALESCE(EXCLUDED.volume, candles.volume),
365
- turnover = COALESCE(EXCLUDED.turnover, candles.turnover),
366
- taker_buy_base_volume = COALESCE(EXCLUDED.taker_buy_base_volume, candles.taker_buy_base_volume),
367
- taker_buy_quote_volume = COALESCE(EXCLUDED.taker_buy_quote_volume, candles.taker_buy_quote_volume),
368
- taker_sell_base_volume = COALESCE(EXCLUDED.taker_sell_base_volume, candles.taker_sell_base_volume),
369
- taker_sell_quote_volume = COALESCE(EXCLUDED.taker_sell_quote_volume, candles.taker_sell_quote_volume)
370
- `;
371
- const client = await pool.connect();
372
- try {
373
- await client.query("BEGIN");
374
- await client.query(sql, flat);
375
- await client.query("COMMIT");
376
- } catch (e) {
377
- await client.query("ROLLBACK");
378
- throw e;
379
- } finally {
380
- client.release();
381
- }
382
- }
383
288
  var ensureDerivativesSchema = async () => {
384
289
  if (derivativesSchemaReady) return;
385
290
  if (derivativesSchemaReadyPromise) {
@@ -1071,44 +976,315 @@ var ensureMarketContextSchemas = async (sources) => {
1071
976
  await ensureMarketContextSchema(source);
1072
977
  }
1073
978
  };
1074
- async function upsertDerivatives(rows) {
979
+
980
+ // src/timescale/candles.ts
981
+ var toRows = (provider, symbol, interval, data) => {
982
+ const normalizedProvider = normalizeCandleProvider(provider);
983
+ if (!normalizedProvider) {
984
+ throw new Error("Candle provider is required");
985
+ }
986
+ const normalizedSymbol = normalizeCandleSymbol(symbol);
987
+ return data.map((i) => ({
988
+ provider: normalizedProvider,
989
+ symbol: normalizedSymbol,
990
+ interval,
991
+ ts: new Date(i.timestamp),
992
+ // ms -> Date
993
+ open: i.open,
994
+ high: i.high,
995
+ low: i.low,
996
+ close: i.close,
997
+ volume: i.volume ?? null,
998
+ turnover: i.turnover ?? null,
999
+ takerBuyBaseVolume: i.takerBuyBaseVolume ?? null,
1000
+ takerBuyQuoteVolume: i.takerBuyQuoteVolume ?? null,
1001
+ takerSellBaseVolume: i.takerSellBaseVolume ?? null,
1002
+ takerSellQuoteVolume: i.takerSellQuoteVolume ?? null
1003
+ }));
1004
+ };
1005
+ async function upsertCandles(rows) {
1075
1006
  if (!rows.length) return;
1076
- await ensureDerivativesSchema();
1007
+ await ensureCandlesSchema();
1077
1008
  const pool = getPool();
1078
1009
  const cols = [
1010
+ "provider",
1079
1011
  "symbol",
1080
1012
  "interval",
1081
1013
  "ts",
1082
- "open_interest",
1083
- "funding_rate",
1084
- "liq_long",
1085
- "liq_short",
1086
- "liq_total",
1087
- "source"
1014
+ "open",
1015
+ "high",
1016
+ "low",
1017
+ "close",
1018
+ "volume",
1019
+ "turnover",
1020
+ "taker_buy_base_volume",
1021
+ "taker_buy_quote_volume",
1022
+ "taker_sell_base_volume",
1023
+ "taker_sell_quote_volume"
1088
1024
  ];
1089
1025
  const maxRows = Math.floor(65535 / cols.length);
1090
1026
  if (rows.length > maxRows) {
1091
1027
  for (let i = 0; i < rows.length; i += maxRows) {
1092
- await upsertDerivatives(rows.slice(i, i + maxRows));
1028
+ await upsertCandles(rows.slice(i, i + maxRows));
1093
1029
  }
1094
1030
  return;
1095
1031
  }
1096
1032
  const valuesSql = rows.map(
1097
1033
  (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1098
1034
  ).join(",");
1099
- const flat = rows.flatMap((row) => [
1100
- row.symbol,
1101
- row.interval,
1102
- row.ts,
1103
- row.openInterest ?? null,
1104
- row.fundingRate ?? null,
1105
- row.liqLong ?? null,
1106
- row.liqShort ?? null,
1107
- row.liqTotal ?? null,
1108
- row.source ?? null
1035
+ const flat = rows.flatMap((r) => [
1036
+ normalizeCandleProvider(r.provider),
1037
+ normalizeCandleSymbol(r.symbol),
1038
+ r.interval,
1039
+ r.ts,
1040
+ r.open,
1041
+ r.high,
1042
+ r.low,
1043
+ r.close,
1044
+ r.volume ?? null,
1045
+ r.turnover ?? null,
1046
+ r.takerBuyBaseVolume ?? null,
1047
+ r.takerBuyQuoteVolume ?? null,
1048
+ r.takerSellBaseVolume ?? null,
1049
+ r.takerSellQuoteVolume ?? null
1109
1050
  ]);
1110
1051
  const sql = `
1111
- INSERT INTO derivatives_market (${cols.join(",")})
1052
+ INSERT INTO candles (${cols.join(",")})
1053
+ VALUES ${valuesSql}
1054
+ ON CONFLICT (provider, symbol, interval, ts) DO UPDATE SET
1055
+ open = EXCLUDED.open,
1056
+ high = EXCLUDED.high,
1057
+ low = EXCLUDED.low,
1058
+ close = EXCLUDED.close,
1059
+ volume = COALESCE(EXCLUDED.volume, candles.volume),
1060
+ turnover = COALESCE(EXCLUDED.turnover, candles.turnover),
1061
+ taker_buy_base_volume = COALESCE(EXCLUDED.taker_buy_base_volume, candles.taker_buy_base_volume),
1062
+ taker_buy_quote_volume = COALESCE(EXCLUDED.taker_buy_quote_volume, candles.taker_buy_quote_volume),
1063
+ taker_sell_base_volume = COALESCE(EXCLUDED.taker_sell_base_volume, candles.taker_sell_base_volume),
1064
+ taker_sell_quote_volume = COALESCE(EXCLUDED.taker_sell_quote_volume, candles.taker_sell_quote_volume)
1065
+ `;
1066
+ const client = await pool.connect();
1067
+ try {
1068
+ await client.query("BEGIN");
1069
+ await client.query(sql, flat);
1070
+ await client.query("COMMIT");
1071
+ } catch (e) {
1072
+ await client.query("ROLLBACK");
1073
+ throw e;
1074
+ } finally {
1075
+ client.release();
1076
+ }
1077
+ }
1078
+ async function getCandlesRange(provider, symbol, interval, startMs, endMs) {
1079
+ await ensureCandlesSchema();
1080
+ const pool = getPool();
1081
+ const normalizedProvider = normalizeCandleProvider(provider);
1082
+ const normalizedSymbol = normalizeCandleSymbol(symbol);
1083
+ const sql = `
1084
+ SELECT symbol, interval, ts,
1085
+ open, high, low, close, volume, turnover,
1086
+ taker_buy_base_volume AS "takerBuyBaseVolume",
1087
+ taker_buy_quote_volume AS "takerBuyQuoteVolume",
1088
+ taker_sell_base_volume AS "takerSellBaseVolume",
1089
+ taker_sell_quote_volume AS "takerSellQuoteVolume"
1090
+ FROM candles
1091
+ WHERE provider = $1 AND symbol = $2 AND interval = $3
1092
+ AND ts >= to_timestamp($4/1000.0)
1093
+ AND ts <= to_timestamp($5/1000.0)
1094
+ ORDER BY ts ASC
1095
+ `;
1096
+ const res = await pool.query(sql, [
1097
+ normalizedProvider,
1098
+ normalizedSymbol,
1099
+ interval,
1100
+ startMs,
1101
+ endMs
1102
+ ]);
1103
+ return res.rows;
1104
+ }
1105
+ async function getDataEdges(provider, symbol, interval) {
1106
+ await ensureCandlesSchema();
1107
+ const pool = getPool();
1108
+ const normalizedProvider = normalizeCandleProvider(provider);
1109
+ const normalizedSymbol = normalizeCandleSymbol(symbol);
1110
+ const sqlMin = `
1111
+ SELECT extract(epoch from ts)*1000 AS ms
1112
+ FROM candles
1113
+ WHERE provider=$1 AND symbol=$2 AND interval=$3
1114
+ ORDER BY ts ASC
1115
+ LIMIT 1
1116
+ `;
1117
+ const sqlMax = `
1118
+ SELECT extract(epoch from ts)*1000 AS ms
1119
+ FROM candles
1120
+ WHERE provider=$1 AND symbol=$2 AND interval=$3
1121
+ ORDER BY ts DESC
1122
+ LIMIT 1
1123
+ `;
1124
+ const [minQ, maxQ] = await Promise.all([
1125
+ pool.query(sqlMin, [normalizedProvider, normalizedSymbol, interval]),
1126
+ pool.query(sqlMax, [normalizedProvider, normalizedSymbol, interval])
1127
+ ]);
1128
+ const minRaw = minQ.rows[0]?.ms;
1129
+ const maxRaw = maxQ.rows[0]?.ms;
1130
+ const min = Number.isFinite(Number(minRaw)) ? Number(minRaw) : void 0;
1131
+ const max = Number.isFinite(Number(maxRaw)) ? Number(maxRaw) : void 0;
1132
+ return { min, max };
1133
+ }
1134
+ async function getDataEdgesForSymbols(provider, symbols, interval) {
1135
+ const normalizedSymbols = [
1136
+ ...new Set(symbols.map(normalizeCandleSymbol).filter(Boolean))
1137
+ ];
1138
+ const result = /* @__PURE__ */ new Map();
1139
+ for (const symbol of normalizedSymbols) {
1140
+ result.set(symbol, {});
1141
+ }
1142
+ if (!normalizedSymbols.length) {
1143
+ return result;
1144
+ }
1145
+ await ensureCandlesSchema();
1146
+ const pool = getPool();
1147
+ const normalizedProvider = normalizeCandleProvider(provider);
1148
+ const sql = `
1149
+ WITH requested(symbol) AS (
1150
+ SELECT unnest($2::text[])
1151
+ )
1152
+ SELECT
1153
+ r.symbol,
1154
+ (
1155
+ SELECT extract(epoch from c.ts)*1000
1156
+ FROM candles c
1157
+ WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
1158
+ ORDER BY c.ts ASC
1159
+ LIMIT 1
1160
+ ) AS min_ms,
1161
+ (
1162
+ SELECT extract(epoch from c.ts)*1000
1163
+ FROM candles c
1164
+ WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
1165
+ ORDER BY c.ts DESC
1166
+ LIMIT 1
1167
+ ) AS max_ms
1168
+ FROM requested r
1169
+ `;
1170
+ const response = await pool.query(sql, [
1171
+ normalizedProvider,
1172
+ normalizedSymbols,
1173
+ interval
1174
+ ]);
1175
+ for (const row of response.rows) {
1176
+ const symbol = normalizeCandleSymbol(String(row.symbol || ""));
1177
+ if (!symbol) continue;
1178
+ const min = row.min_ms == null ? NaN : Number(row.min_ms);
1179
+ const max = row.max_ms == null ? NaN : Number(row.max_ms);
1180
+ result.set(symbol, {
1181
+ ...Number.isFinite(min) ? { min } : {},
1182
+ ...Number.isFinite(max) ? { max } : {}
1183
+ });
1184
+ }
1185
+ return result;
1186
+ }
1187
+ async function waitForDbReady(attempts = 20, delayMs = 1e3) {
1188
+ const pool = getPool();
1189
+ let lastError;
1190
+ for (let i = 0; i < attempts; i++) {
1191
+ try {
1192
+ await pool.query("SELECT 1");
1193
+ return;
1194
+ } catch (e) {
1195
+ lastError = e;
1196
+ await new Promise((r) => setTimeout(r, delayMs));
1197
+ }
1198
+ }
1199
+ throw lastError;
1200
+ }
1201
+ async function deleteCandles(provider, symbol, interval) {
1202
+ const pool = getPool();
1203
+ const normalizedProvider = normalizeCandleProvider(provider);
1204
+ const normalizedSymbol = normalizeCandleSymbol(symbol);
1205
+ const sql = `
1206
+ DELETE FROM candles
1207
+ WHERE provider = $1 AND symbol = $2 AND interval = $3
1208
+ `;
1209
+ await pool.query(sql, [normalizedProvider, normalizedSymbol, interval]);
1210
+ }
1211
+ async function findContinuityGap(provider, symbol, interval) {
1212
+ const pool = getPool();
1213
+ const normalizedProvider = normalizeCandleProvider(provider);
1214
+ const normalizedSymbol = normalizeCandleSymbol(symbol);
1215
+ const expectedSeconds = interval * 60;
1216
+ const sql = `
1217
+ WITH ordered AS (
1218
+ SELECT
1219
+ ts,
1220
+ LAG(ts) OVER (ORDER BY ts) AS prev_ts
1221
+ FROM candles
1222
+ WHERE provider = $1 AND symbol = $2 AND interval = $3
1223
+ )
1224
+ SELECT
1225
+ ts,
1226
+ prev_ts,
1227
+ EXTRACT(EPOCH FROM (ts - prev_ts))::int AS diff_seconds
1228
+ FROM ordered
1229
+ WHERE prev_ts IS NOT NULL
1230
+ AND EXTRACT(EPOCH FROM (ts - prev_ts))::int <> $4
1231
+ ORDER BY ts ASC
1232
+ LIMIT 1
1233
+ `;
1234
+ const res = await pool.query(sql, [
1235
+ normalizedProvider,
1236
+ normalizedSymbol,
1237
+ interval,
1238
+ expectedSeconds
1239
+ ]);
1240
+ const row = res.rows[0];
1241
+ if (!row) return null;
1242
+ return {
1243
+ ts: new Date(row.ts).getTime(),
1244
+ prevTs: new Date(row.prev_ts).getTime(),
1245
+ diffSeconds: row.diff_seconds
1246
+ };
1247
+ }
1248
+
1249
+ // src/timescale/derivatives.ts
1250
+ async function upsertDerivatives(rows) {
1251
+ if (!rows.length) return;
1252
+ await ensureDerivativesSchema();
1253
+ const pool = getPool();
1254
+ const cols = [
1255
+ "symbol",
1256
+ "interval",
1257
+ "ts",
1258
+ "open_interest",
1259
+ "funding_rate",
1260
+ "liq_long",
1261
+ "liq_short",
1262
+ "liq_total",
1263
+ "source"
1264
+ ];
1265
+ const maxRows = Math.floor(65535 / cols.length);
1266
+ if (rows.length > maxRows) {
1267
+ for (let i = 0; i < rows.length; i += maxRows) {
1268
+ await upsertDerivatives(rows.slice(i, i + maxRows));
1269
+ }
1270
+ return;
1271
+ }
1272
+ const valuesSql = rows.map(
1273
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1274
+ ).join(",");
1275
+ const flat = rows.flatMap((row) => [
1276
+ row.symbol,
1277
+ row.interval,
1278
+ row.ts,
1279
+ row.openInterest ?? null,
1280
+ row.fundingRate ?? null,
1281
+ row.liqLong ?? null,
1282
+ row.liqShort ?? null,
1283
+ row.liqTotal ?? null,
1284
+ row.source ?? null
1285
+ ]);
1286
+ const sql = `
1287
+ INSERT INTO derivatives_market (${cols.join(",")})
1112
1288
  VALUES ${valuesSql}
1113
1289
  ON CONFLICT (symbol, interval, ts) DO UPDATE SET
1114
1290
  open_interest = COALESCE(EXCLUDED.open_interest, derivatives_market.open_interest),
@@ -1542,6 +1718,8 @@ async function getDerivativesSummary(hours = 24, limit = 500, symbols) {
1542
1718
  items
1543
1719
  };
1544
1720
  }
1721
+
1722
+ // src/timescale/spread.ts
1545
1723
  async function upsertSpreadRows(rows) {
1546
1724
  if (!rows.length) return;
1547
1725
  await ensureSpreadSchema();
@@ -1586,23 +1764,81 @@ async function upsertSpreadRows(rows) {
1586
1764
  `;
1587
1765
  await pool.query(sql, flat);
1588
1766
  }
1589
- async function upsertMarketTradeFlowRows(rows) {
1590
- if (!rows.length) return;
1591
- await ensureBinanceMarketSchema();
1767
+ async function getSpreadRangeForSymbols(symbols, interval, startMs, endMs) {
1768
+ if (!symbols.length) {
1769
+ return [];
1770
+ }
1771
+ await ensureSpreadSchema();
1592
1772
  const pool = getPool();
1593
- const cols = [
1594
- "symbol",
1595
- "interval",
1596
- "ts",
1597
- "trades",
1598
- "buy_base_volume",
1599
- "sell_base_volume",
1600
- "buy_quote_volume",
1601
- "sell_quote_volume",
1602
- "net_base_delta",
1603
- "net_quote_delta",
1604
- "buy_pressure_pct",
1605
- "source"
1773
+ const sql = `
1774
+ SELECT symbol, interval, ts, binance_price, coinbase_price, spread
1775
+ FROM market_spread
1776
+ WHERE symbol = ANY($1)
1777
+ AND interval = $2
1778
+ AND ts >= to_timestamp($3/1000.0)
1779
+ AND ts <= to_timestamp($4/1000.0)
1780
+ ORDER BY symbol ASC, ts ASC
1781
+ `;
1782
+ const res = await pool.query(sql, [symbols, interval, startMs, endMs]);
1783
+ return res.rows;
1784
+ }
1785
+ async function getSpreadSummary(hours = 24, limit = 500) {
1786
+ await ensureSpreadSchema();
1787
+ const pool = getPool();
1788
+ const cappedHours = Math.max(1, Math.min(24 * 30, hours));
1789
+ const cappedLimit = Math.max(50, Math.min(5e3, limit));
1790
+ const rowsQ = await pool.query(
1791
+ `
1792
+ SELECT symbol, interval, ts, binance_price, coinbase_price, spread
1793
+ FROM market_spread
1794
+ WHERE ts >= now() - ($1 || ' hours')::interval
1795
+ ORDER BY ts DESC
1796
+ LIMIT $2
1797
+ `,
1798
+ [String(cappedHours), cappedLimit]
1799
+ );
1800
+ const aggQ = await pool.query(
1801
+ `
1802
+ SELECT
1803
+ symbol,
1804
+ interval,
1805
+ COUNT(*)::int AS points,
1806
+ MAX(ts) AS last_ts,
1807
+ AVG(spread) AS avg_spread,
1808
+ STDDEV_POP(spread) AS std_spread
1809
+ FROM market_spread
1810
+ WHERE ts >= now() - ($1 || ' hours')::interval
1811
+ GROUP BY symbol, interval
1812
+ ORDER BY points DESC, symbol ASC
1813
+ LIMIT 500
1814
+ `,
1815
+ [String(cappedHours)]
1816
+ );
1817
+ return {
1818
+ rows: rowsQ.rows,
1819
+ aggregates: aggQ.rows,
1820
+ hours: cappedHours
1821
+ };
1822
+ }
1823
+
1824
+ // src/timescale/marketContext.ts
1825
+ async function upsertMarketTradeFlowRows(rows) {
1826
+ if (!rows.length) return;
1827
+ await ensureBinanceMarketSchema();
1828
+ const pool = getPool();
1829
+ const cols = [
1830
+ "symbol",
1831
+ "interval",
1832
+ "ts",
1833
+ "trades",
1834
+ "buy_base_volume",
1835
+ "sell_base_volume",
1836
+ "buy_quote_volume",
1837
+ "sell_quote_volume",
1838
+ "net_base_delta",
1839
+ "net_quote_delta",
1840
+ "buy_pressure_pct",
1841
+ "source"
1606
1842
  ];
1607
1843
  const maxRows = getSafeBulkInsertRows(cols.length);
1608
1844
  if (rows.length > maxRows) {
@@ -1647,530 +1883,273 @@ async function upsertMarketTradeFlowRows(rows) {
1647
1883
  flat
1648
1884
  );
1649
1885
  }
1650
- async function upsertHyperliquidWhaleTradeEvents(rows) {
1886
+ async function upsertMarketBreadthRows(rows) {
1651
1887
  if (!rows.length) return;
1652
- await ensureHyperliquidWhaleSchema();
1888
+ await ensureBinanceMarketSchema();
1889
+ const pool = getPool();
1653
1890
  const cols = [
1654
- "symbol",
1891
+ "universe",
1892
+ "interval",
1655
1893
  "ts",
1656
- "tid",
1657
- "price",
1658
- "size",
1659
- "notional_usd",
1660
- "buyer_address",
1661
- "seller_address",
1662
- "buyer_tracked",
1663
- "seller_tracked",
1664
- "buyer_start_position",
1665
- "buyer_end_position",
1666
- "buyer_position_action",
1667
- "buyer_closed_pnl",
1668
- "buyer_liquidation",
1669
- "seller_start_position",
1670
- "seller_end_position",
1671
- "seller_position_action",
1672
- "seller_closed_pnl",
1673
- "seller_liquidation",
1674
- "universe_fingerprint",
1675
- "whale_registry_fingerprint",
1894
+ "symbols_count",
1895
+ "advancers",
1896
+ "decliners",
1897
+ "unchanged",
1898
+ "advance_decline_ratio",
1899
+ "pct_above_ma20",
1900
+ "pct_above_ma50",
1901
+ "equal_weighted_return",
1902
+ "volume_weighted_return",
1903
+ "dispersion",
1904
+ "btc_return_1h",
1905
+ "btc_return_4h",
1906
+ "btc_return_24h",
1907
+ "alt_basket_return_1h",
1908
+ "alt_basket_return_4h",
1909
+ "alt_basket_return_24h",
1910
+ "btc_vs_alt_return_1h",
1911
+ "btc_vs_alt_return_4h",
1912
+ "btc_vs_alt_return_24h",
1913
+ "btc_turnover_share_1h",
1914
+ "btc_turnover_share_24h",
1915
+ "btc_turnover_share_change_24h",
1916
+ "alt_vol_to_btc_vol_24h",
1917
+ "alt_dispersion_24h",
1918
+ "btc_alt_regime",
1676
1919
  "source"
1677
1920
  ];
1678
1921
  const maxRows = getSafeBulkInsertRows(cols.length);
1679
1922
  if (rows.length > maxRows) {
1680
- for (let index = 0; index < rows.length; index += maxRows) {
1681
- await upsertHyperliquidWhaleTradeEvents(
1682
- rows.slice(index, index + maxRows)
1683
- );
1923
+ for (let i = 0; i < rows.length; i += maxRows) {
1924
+ await upsertMarketBreadthRows(rows.slice(i, i + maxRows));
1684
1925
  }
1685
1926
  return;
1686
1927
  }
1687
1928
  const valuesSql = rows.map(
1688
- (_, rowIndex) => `(${cols.map((__, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(",")})`
1929
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1689
1930
  ).join(",");
1690
1931
  const flat = rows.flatMap((row) => [
1691
- row.symbol,
1932
+ row.universe,
1933
+ row.interval,
1692
1934
  row.ts,
1693
- row.tid,
1694
- row.price,
1695
- row.size,
1696
- row.notionalUsd,
1697
- row.buyerAddress ?? null,
1698
- row.sellerAddress ?? null,
1699
- row.buyerTracked,
1700
- row.sellerTracked,
1701
- row.buyerStartPosition ?? null,
1702
- row.buyerEndPosition ?? null,
1703
- row.buyerPositionAction ?? null,
1704
- row.buyerClosedPnl ?? null,
1705
- row.buyerLiquidation ?? null,
1706
- row.sellerStartPosition ?? null,
1707
- row.sellerEndPosition ?? null,
1708
- row.sellerPositionAction ?? null,
1709
- row.sellerClosedPnl ?? null,
1710
- row.sellerLiquidation ?? null,
1711
- row.universeFingerprint,
1712
- row.whaleRegistryFingerprint,
1935
+ row.symbolsCount,
1936
+ row.advancers,
1937
+ row.decliners,
1938
+ row.unchanged,
1939
+ row.advanceDeclineRatio ?? null,
1940
+ row.pctAboveMa20 ?? null,
1941
+ row.pctAboveMa50 ?? null,
1942
+ row.equalWeightedReturn ?? null,
1943
+ row.volumeWeightedReturn ?? null,
1944
+ row.dispersion ?? null,
1945
+ row.btcReturn1h ?? null,
1946
+ row.btcReturn4h ?? null,
1947
+ row.btcReturn24h ?? null,
1948
+ row.altBasketReturn1h ?? null,
1949
+ row.altBasketReturn4h ?? null,
1950
+ row.altBasketReturn24h ?? null,
1951
+ row.btcVsAltReturn1h ?? null,
1952
+ row.btcVsAltReturn4h ?? null,
1953
+ row.btcVsAltReturn24h ?? null,
1954
+ row.btcTurnoverShare1h ?? null,
1955
+ row.btcTurnoverShare24h ?? null,
1956
+ row.btcTurnoverShareChange24h ?? null,
1957
+ row.altVolToBtcVol24h ?? null,
1958
+ row.altDispersion24h ?? null,
1959
+ row.btcAltRegime ?? null,
1713
1960
  row.source ?? null
1714
1961
  ]);
1715
- await getPool().query(
1962
+ await pool.query(
1716
1963
  `
1717
- INSERT INTO hyperliquid_whale_trade_events (${cols.join(",")})
1964
+ INSERT INTO market_breadth (${cols.join(",")})
1718
1965
  VALUES ${valuesSql}
1719
- ON CONFLICT (
1720
- universe_fingerprint,
1721
- whale_registry_fingerprint,
1722
- symbol,
1723
- ts,
1724
- tid
1725
- ) DO UPDATE SET
1726
- buyer_address = COALESCE(
1727
- hyperliquid_whale_trade_events.buyer_address,
1728
- EXCLUDED.buyer_address
1729
- ),
1730
- seller_address = COALESCE(
1731
- hyperliquid_whale_trade_events.seller_address,
1732
- EXCLUDED.seller_address
1733
- ),
1734
- buyer_tracked = hyperliquid_whale_trade_events.buyer_tracked OR EXCLUDED.buyer_tracked,
1735
- seller_tracked = hyperliquid_whale_trade_events.seller_tracked OR EXCLUDED.seller_tracked,
1736
- buyer_start_position = COALESCE(
1737
- hyperliquid_whale_trade_events.buyer_start_position,
1738
- EXCLUDED.buyer_start_position
1739
- ),
1740
- buyer_end_position = COALESCE(
1741
- hyperliquid_whale_trade_events.buyer_end_position,
1742
- EXCLUDED.buyer_end_position
1743
- ),
1744
- buyer_position_action = COALESCE(
1745
- hyperliquid_whale_trade_events.buyer_position_action,
1746
- EXCLUDED.buyer_position_action
1747
- ),
1748
- buyer_closed_pnl = COALESCE(
1749
- hyperliquid_whale_trade_events.buyer_closed_pnl,
1750
- EXCLUDED.buyer_closed_pnl
1751
- ),
1752
- buyer_liquidation = COALESCE(
1753
- hyperliquid_whale_trade_events.buyer_liquidation,
1754
- EXCLUDED.buyer_liquidation
1755
- ),
1756
- seller_start_position = COALESCE(
1757
- hyperliquid_whale_trade_events.seller_start_position,
1758
- EXCLUDED.seller_start_position
1759
- ),
1760
- seller_end_position = COALESCE(
1761
- hyperliquid_whale_trade_events.seller_end_position,
1762
- EXCLUDED.seller_end_position
1763
- ),
1764
- seller_position_action = COALESCE(
1765
- hyperliquid_whale_trade_events.seller_position_action,
1766
- EXCLUDED.seller_position_action
1767
- ),
1768
- seller_closed_pnl = COALESCE(
1769
- hyperliquid_whale_trade_events.seller_closed_pnl,
1770
- EXCLUDED.seller_closed_pnl
1771
- ),
1772
- seller_liquidation = COALESCE(
1773
- hyperliquid_whale_trade_events.seller_liquidation,
1774
- EXCLUDED.seller_liquidation
1775
- ),
1776
- source = EXCLUDED.source,
1777
- ingested_at = now()
1778
- `,
1779
- flat
1780
- );
1781
- }
1782
- async function upsertHyperliquidWhaleFlowRows(rows) {
1783
- if (!rows.length) return;
1784
- await ensureHyperliquidWhaleSchema();
1785
- const cols = [
1786
- "symbol",
1787
- "interval",
1788
- "ts",
1789
- "trades",
1790
- "whale_sides",
1791
- "unique_whales",
1792
- "whale_addresses",
1793
- "buy_notional_usd",
1794
- "sell_notional_usd",
1795
- "net_notional_usd",
1796
- "buy_share_pct",
1797
- "position_aware_whale_sides",
1798
- "long_entry_whale_addresses",
1799
- "short_entry_whale_addresses",
1800
- "long_exit_whale_addresses",
1801
- "short_exit_whale_addresses",
1802
- "long_entry_notional_usd",
1803
- "short_entry_notional_usd",
1804
- "long_exit_notional_usd",
1805
- "short_exit_notional_usd",
1806
- "entry_net_notional_usd",
1807
- "entry_long_share_pct",
1808
- "universe_fingerprint",
1809
- "whale_registry_fingerprint",
1810
- "source"
1966
+ ON CONFLICT (universe, interval, ts) DO UPDATE SET
1967
+ symbols_count = EXCLUDED.symbols_count,
1968
+ advancers = EXCLUDED.advancers,
1969
+ decliners = EXCLUDED.decliners,
1970
+ unchanged = EXCLUDED.unchanged,
1971
+ advance_decline_ratio = COALESCE(EXCLUDED.advance_decline_ratio, market_breadth.advance_decline_ratio),
1972
+ pct_above_ma20 = COALESCE(EXCLUDED.pct_above_ma20, market_breadth.pct_above_ma20),
1973
+ pct_above_ma50 = COALESCE(EXCLUDED.pct_above_ma50, market_breadth.pct_above_ma50),
1974
+ equal_weighted_return = COALESCE(EXCLUDED.equal_weighted_return, market_breadth.equal_weighted_return),
1975
+ volume_weighted_return = COALESCE(EXCLUDED.volume_weighted_return, market_breadth.volume_weighted_return),
1976
+ dispersion = COALESCE(EXCLUDED.dispersion, market_breadth.dispersion),
1977
+ btc_return_1h = COALESCE(EXCLUDED.btc_return_1h, market_breadth.btc_return_1h),
1978
+ btc_return_4h = COALESCE(EXCLUDED.btc_return_4h, market_breadth.btc_return_4h),
1979
+ btc_return_24h = COALESCE(EXCLUDED.btc_return_24h, market_breadth.btc_return_24h),
1980
+ alt_basket_return_1h = COALESCE(EXCLUDED.alt_basket_return_1h, market_breadth.alt_basket_return_1h),
1981
+ alt_basket_return_4h = COALESCE(EXCLUDED.alt_basket_return_4h, market_breadth.alt_basket_return_4h),
1982
+ alt_basket_return_24h = COALESCE(EXCLUDED.alt_basket_return_24h, market_breadth.alt_basket_return_24h),
1983
+ btc_vs_alt_return_1h = COALESCE(EXCLUDED.btc_vs_alt_return_1h, market_breadth.btc_vs_alt_return_1h),
1984
+ btc_vs_alt_return_4h = COALESCE(EXCLUDED.btc_vs_alt_return_4h, market_breadth.btc_vs_alt_return_4h),
1985
+ btc_vs_alt_return_24h = COALESCE(EXCLUDED.btc_vs_alt_return_24h, market_breadth.btc_vs_alt_return_24h),
1986
+ btc_turnover_share_1h = COALESCE(EXCLUDED.btc_turnover_share_1h, market_breadth.btc_turnover_share_1h),
1987
+ btc_turnover_share_24h = COALESCE(EXCLUDED.btc_turnover_share_24h, market_breadth.btc_turnover_share_24h),
1988
+ btc_turnover_share_change_24h = COALESCE(EXCLUDED.btc_turnover_share_change_24h, market_breadth.btc_turnover_share_change_24h),
1989
+ alt_vol_to_btc_vol_24h = COALESCE(EXCLUDED.alt_vol_to_btc_vol_24h, market_breadth.alt_vol_to_btc_vol_24h),
1990
+ alt_dispersion_24h = COALESCE(EXCLUDED.alt_dispersion_24h, market_breadth.alt_dispersion_24h),
1991
+ btc_alt_regime = COALESCE(EXCLUDED.btc_alt_regime, market_breadth.btc_alt_regime),
1992
+ source = COALESCE(EXCLUDED.source, market_breadth.source),
1993
+ ingested_at = now()
1994
+ `,
1995
+ flat
1996
+ );
1997
+ }
1998
+ async function upsertMarketGlobalContextRows(rows) {
1999
+ if (!rows.length) return;
2000
+ await ensureBinanceMarketSchema();
2001
+ const pool = getPool();
2002
+ const cols = [
2003
+ "source",
2004
+ "ts",
2005
+ "updated_at_ts",
2006
+ "active_cryptocurrencies",
2007
+ "active_exchanges",
2008
+ "active_market_pairs",
2009
+ "markets",
2010
+ "total_market_cap_usd",
2011
+ "total_volume_usd",
2012
+ "total_volume_reported_usd",
2013
+ "btc_dominance_pct",
2014
+ "eth_dominance_pct",
2015
+ "alt_market_cap_usd",
2016
+ "alt_volume_usd",
2017
+ "alt_volume_reported_usd",
2018
+ "btc_to_alt_market_cap_ratio",
2019
+ "market_cap_change_pct_24h_usd"
1811
2020
  ];
1812
2021
  const maxRows = getSafeBulkInsertRows(cols.length);
1813
2022
  if (rows.length > maxRows) {
1814
- for (let index = 0; index < rows.length; index += maxRows) {
1815
- await upsertHyperliquidWhaleFlowRows(rows.slice(index, index + maxRows));
2023
+ for (let i = 0; i < rows.length; i += maxRows) {
2024
+ await upsertMarketGlobalContextRows(rows.slice(i, i + maxRows));
1816
2025
  }
1817
2026
  return;
1818
2027
  }
1819
2028
  const valuesSql = rows.map(
1820
- (_, rowIndex) => `(${cols.map((__, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(",")})`
2029
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
1821
2030
  ).join(",");
1822
2031
  const flat = rows.flatMap((row) => [
1823
- row.symbol,
2032
+ row.source,
2033
+ row.ts,
2034
+ row.updatedAt ?? null,
2035
+ row.activeCryptocurrencies ?? null,
2036
+ row.activeExchanges ?? null,
2037
+ row.activeMarketPairs ?? null,
2038
+ row.markets ?? null,
2039
+ row.totalMarketCapUsd ?? null,
2040
+ row.totalVolumeUsd ?? null,
2041
+ row.totalVolumeReportedUsd ?? null,
2042
+ row.btcDominancePct ?? null,
2043
+ row.ethDominancePct ?? null,
2044
+ row.altMarketCapUsd ?? null,
2045
+ row.altVolumeUsd ?? null,
2046
+ row.altVolumeReportedUsd ?? null,
2047
+ row.btcToAltMarketCapRatio ?? null,
2048
+ row.marketCapChangePct24hUsd ?? null
2049
+ ]);
2050
+ await pool.query(
2051
+ `
2052
+ INSERT INTO market_global_context (${cols.join(",")})
2053
+ VALUES ${valuesSql}
2054
+ ON CONFLICT (source, ts) DO UPDATE SET
2055
+ updated_at_ts = COALESCE(EXCLUDED.updated_at_ts, market_global_context.updated_at_ts),
2056
+ active_cryptocurrencies = COALESCE(EXCLUDED.active_cryptocurrencies, market_global_context.active_cryptocurrencies),
2057
+ active_exchanges = COALESCE(EXCLUDED.active_exchanges, market_global_context.active_exchanges),
2058
+ active_market_pairs = COALESCE(EXCLUDED.active_market_pairs, market_global_context.active_market_pairs),
2059
+ markets = COALESCE(EXCLUDED.markets, market_global_context.markets),
2060
+ total_market_cap_usd = COALESCE(EXCLUDED.total_market_cap_usd, market_global_context.total_market_cap_usd),
2061
+ total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_global_context.total_volume_usd),
2062
+ total_volume_reported_usd = COALESCE(EXCLUDED.total_volume_reported_usd, market_global_context.total_volume_reported_usd),
2063
+ btc_dominance_pct = COALESCE(EXCLUDED.btc_dominance_pct, market_global_context.btc_dominance_pct),
2064
+ eth_dominance_pct = COALESCE(EXCLUDED.eth_dominance_pct, market_global_context.eth_dominance_pct),
2065
+ alt_market_cap_usd = COALESCE(EXCLUDED.alt_market_cap_usd, market_global_context.alt_market_cap_usd),
2066
+ alt_volume_usd = COALESCE(EXCLUDED.alt_volume_usd, market_global_context.alt_volume_usd),
2067
+ alt_volume_reported_usd = COALESCE(EXCLUDED.alt_volume_reported_usd, market_global_context.alt_volume_reported_usd),
2068
+ btc_to_alt_market_cap_ratio = COALESCE(EXCLUDED.btc_to_alt_market_cap_ratio, market_global_context.btc_to_alt_market_cap_ratio),
2069
+ market_cap_change_pct_24h_usd = COALESCE(EXCLUDED.market_cap_change_pct_24h_usd, market_global_context.market_cap_change_pct_24h_usd),
2070
+ ingested_at = now()
2071
+ `,
2072
+ flat
2073
+ );
2074
+ }
2075
+ async function upsertMarketReferenceAssetContextRows(rows) {
2076
+ if (!rows.length) return;
2077
+ await ensureBinanceMarketSchema();
2078
+ const pool = getPool();
2079
+ const cols = [
2080
+ "source",
2081
+ "symbol",
2082
+ "cmc_id",
2083
+ "interval",
2084
+ "ts",
2085
+ "open_usd",
2086
+ "high_usd",
2087
+ "low_usd",
2088
+ "close_usd",
2089
+ "volume_usd",
2090
+ "market_cap_usd"
2091
+ ];
2092
+ const maxRows = getSafeBulkInsertRows(cols.length);
2093
+ if (rows.length > maxRows) {
2094
+ for (let i = 0; i < rows.length; i += maxRows) {
2095
+ await upsertMarketReferenceAssetContextRows(rows.slice(i, i + maxRows));
2096
+ }
2097
+ return;
2098
+ }
2099
+ const valuesSql = rows.map(
2100
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2101
+ ).join(",");
2102
+ const flat = rows.flatMap((row) => [
2103
+ row.source,
2104
+ row.symbol.trim().toUpperCase(),
2105
+ Math.trunc(row.cmcId),
1824
2106
  row.interval,
1825
2107
  row.ts,
1826
- row.trades,
1827
- row.whaleSides,
1828
- row.uniqueWhales,
1829
- row.whaleAddresses ?? [],
1830
- row.buyNotionalUsd,
1831
- row.sellNotionalUsd,
1832
- row.netNotionalUsd,
1833
- row.buySharePct ?? null,
1834
- row.positionAwareWhaleSides,
1835
- row.longEntryWhaleAddresses ?? [],
1836
- row.shortEntryWhaleAddresses ?? [],
1837
- row.longExitWhaleAddresses ?? [],
1838
- row.shortExitWhaleAddresses ?? [],
1839
- row.longEntryNotionalUsd,
1840
- row.shortEntryNotionalUsd,
1841
- row.longExitNotionalUsd,
1842
- row.shortExitNotionalUsd,
1843
- row.entryNetNotionalUsd,
1844
- row.entryLongSharePct ?? null,
1845
- row.universeFingerprint,
1846
- row.whaleRegistryFingerprint,
1847
- row.source ?? null
2108
+ row.openUsd ?? null,
2109
+ row.highUsd ?? null,
2110
+ row.lowUsd ?? null,
2111
+ row.closeUsd ?? null,
2112
+ row.volumeUsd ?? null,
2113
+ row.marketCapUsd ?? null
1848
2114
  ]);
1849
- await getPool().query(
2115
+ await pool.query(
1850
2116
  `
1851
- INSERT INTO hyperliquid_whale_flow (${cols.join(",")})
2117
+ INSERT INTO market_reference_asset_context (${cols.join(",")})
1852
2118
  VALUES ${valuesSql}
1853
- ON CONFLICT (
1854
- universe_fingerprint,
1855
- whale_registry_fingerprint,
1856
- symbol,
1857
- interval,
1858
- ts
1859
- ) DO UPDATE SET
1860
- trades = EXCLUDED.trades,
1861
- whale_sides = EXCLUDED.whale_sides,
1862
- unique_whales = EXCLUDED.unique_whales,
1863
- whale_addresses = EXCLUDED.whale_addresses,
1864
- buy_notional_usd = EXCLUDED.buy_notional_usd,
1865
- sell_notional_usd = EXCLUDED.sell_notional_usd,
1866
- net_notional_usd = EXCLUDED.net_notional_usd,
1867
- buy_share_pct = EXCLUDED.buy_share_pct,
1868
- position_aware_whale_sides = EXCLUDED.position_aware_whale_sides,
1869
- long_entry_whale_addresses = EXCLUDED.long_entry_whale_addresses,
1870
- short_entry_whale_addresses = EXCLUDED.short_entry_whale_addresses,
1871
- long_exit_whale_addresses = EXCLUDED.long_exit_whale_addresses,
1872
- short_exit_whale_addresses = EXCLUDED.short_exit_whale_addresses,
1873
- long_entry_notional_usd = EXCLUDED.long_entry_notional_usd,
1874
- short_entry_notional_usd = EXCLUDED.short_entry_notional_usd,
1875
- long_exit_notional_usd = EXCLUDED.long_exit_notional_usd,
1876
- short_exit_notional_usd = EXCLUDED.short_exit_notional_usd,
1877
- entry_net_notional_usd = EXCLUDED.entry_net_notional_usd,
1878
- entry_long_share_pct = EXCLUDED.entry_long_share_pct,
1879
- source = EXCLUDED.source,
2119
+ ON CONFLICT (source, symbol, interval, ts) DO UPDATE SET
2120
+ cmc_id = EXCLUDED.cmc_id,
2121
+ open_usd = COALESCE(EXCLUDED.open_usd, market_reference_asset_context.open_usd),
2122
+ high_usd = COALESCE(EXCLUDED.high_usd, market_reference_asset_context.high_usd),
2123
+ low_usd = COALESCE(EXCLUDED.low_usd, market_reference_asset_context.low_usd),
2124
+ close_usd = COALESCE(EXCLUDED.close_usd, market_reference_asset_context.close_usd),
2125
+ volume_usd = COALESCE(EXCLUDED.volume_usd, market_reference_asset_context.volume_usd),
2126
+ market_cap_usd = COALESCE(EXCLUDED.market_cap_usd, market_reference_asset_context.market_cap_usd),
1880
2127
  ingested_at = now()
1881
2128
  `,
1882
2129
  flat
1883
2130
  );
1884
2131
  }
1885
- async function rebuildHyperliquidWhaleFlowRows(params) {
1886
- await ensureHyperliquidWhaleSchema();
1887
- const client = await getPool().connect();
1888
- try {
1889
- await client.query("BEGIN");
1890
- const result = await client.query(
1891
- `
1892
- WITH source_events AS (
1893
- SELECT *
1894
- FROM hyperliquid_whale_trade_events
1895
- WHERE universe_fingerprint = $1
1896
- AND whale_registry_fingerprint = $2
1897
- AND ts >= to_timestamp($3/1000.0)
1898
- AND ts < to_timestamp($4/1000.0)
1899
- ), metrics AS (
1900
- SELECT
1901
- symbol,
1902
- date_trunc('minute', ts) AS bucket_ts,
1903
- COUNT(*)::int AS trades,
1904
- SUM(buyer_tracked::int + seller_tracked::int)::int AS whale_sides,
1905
- SUM(CASE WHEN buyer_tracked THEN notional_usd ELSE 0 END) AS buy_notional_usd,
1906
- SUM(CASE WHEN seller_tracked THEN notional_usd ELSE 0 END) AS sell_notional_usd
1907
- FROM source_events
1908
- GROUP BY symbol, date_trunc('minute', ts)
1909
- ), position_legs AS (
1910
- SELECT
1911
- symbol,
1912
- ts,
1913
- price,
1914
- buyer_address AS whale_address,
1915
- buyer_start_position AS start_position,
1916
- buyer_end_position AS end_position
1917
- FROM source_events
1918
- WHERE buyer_tracked
1919
- AND buyer_address IS NOT NULL
1920
- AND buyer_start_position IS NOT NULL
1921
- AND buyer_end_position IS NOT NULL
1922
- UNION ALL
1923
- SELECT
1924
- symbol,
1925
- ts,
1926
- price,
1927
- seller_address AS whale_address,
1928
- seller_start_position AS start_position,
1929
- seller_end_position AS end_position
1930
- FROM source_events
1931
- WHERE seller_tracked
1932
- AND seller_address IS NOT NULL
1933
- AND seller_start_position IS NOT NULL
1934
- AND seller_end_position IS NOT NULL
1935
- ), classified_legs AS (
1936
- SELECT
1937
- *,
1938
- GREATEST(
1939
- GREATEST(end_position, 0) - GREATEST(start_position, 0),
1940
- 0
1941
- ) AS long_entry_size,
1942
- GREATEST(
1943
- GREATEST(-end_position, 0) - GREATEST(-start_position, 0),
1944
- 0
1945
- ) AS short_entry_size,
1946
- GREATEST(
1947
- GREATEST(start_position, 0) - GREATEST(end_position, 0),
1948
- 0
1949
- ) AS long_exit_size,
1950
- GREATEST(
1951
- GREATEST(-start_position, 0) - GREATEST(-end_position, 0),
1952
- 0
1953
- ) AS short_exit_size
1954
- FROM position_legs
1955
- ), position_metrics AS (
1956
- SELECT
1957
- symbol,
1958
- date_trunc('minute', ts) AS bucket_ts,
1959
- COUNT(*)::int AS position_aware_whale_sides,
1960
- SUM(long_entry_size * price) AS long_entry_notional_usd,
1961
- SUM(short_entry_size * price) AS short_entry_notional_usd,
1962
- SUM(long_exit_size * price) AS long_exit_notional_usd,
1963
- SUM(short_exit_size * price) AS short_exit_notional_usd,
1964
- COALESCE(
1965
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1966
- FILTER (WHERE long_entry_size > 0),
1967
- '{}'
1968
- ) AS long_entry_whale_addresses,
1969
- COALESCE(
1970
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1971
- FILTER (WHERE short_entry_size > 0),
1972
- '{}'
1973
- ) AS short_entry_whale_addresses,
1974
- COALESCE(
1975
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1976
- FILTER (WHERE long_exit_size > 0),
1977
- '{}'
1978
- ) AS long_exit_whale_addresses,
1979
- COALESCE(
1980
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
1981
- FILTER (WHERE short_exit_size > 0),
1982
- '{}'
1983
- ) AS short_exit_whale_addresses
1984
- FROM classified_legs
1985
- GROUP BY symbol, date_trunc('minute', ts)
1986
- ), addresses AS (
1987
- SELECT
1988
- symbol,
1989
- date_trunc('minute', ts) AS bucket_ts,
1990
- ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address) AS whale_addresses
1991
- FROM source_events
1992
- CROSS JOIN LATERAL UNNEST(ARRAY[
1993
- CASE WHEN buyer_tracked THEN buyer_address END,
1994
- CASE WHEN seller_tracked THEN seller_address END
1995
- ]) AS expanded(whale_address)
1996
- WHERE whale_address IS NOT NULL
1997
- GROUP BY symbol, date_trunc('minute', ts)
1998
- )
1999
- INSERT INTO hyperliquid_whale_flow (
2000
- symbol,
2001
- interval,
2002
- ts,
2003
- trades,
2004
- whale_sides,
2005
- unique_whales,
2006
- whale_addresses,
2007
- buy_notional_usd,
2008
- sell_notional_usd,
2009
- net_notional_usd,
2010
- buy_share_pct,
2011
- position_aware_whale_sides,
2012
- long_entry_whale_addresses,
2013
- short_entry_whale_addresses,
2014
- long_exit_whale_addresses,
2015
- short_exit_whale_addresses,
2016
- long_entry_notional_usd,
2017
- short_entry_notional_usd,
2018
- long_exit_notional_usd,
2019
- short_exit_notional_usd,
2020
- entry_net_notional_usd,
2021
- entry_long_share_pct,
2022
- universe_fingerprint,
2023
- whale_registry_fingerprint,
2024
- source
2025
- )
2026
- SELECT
2027
- metrics.symbol,
2028
- '1m',
2029
- metrics.bucket_ts,
2030
- metrics.trades,
2031
- metrics.whale_sides,
2032
- COALESCE(CARDINALITY(addresses.whale_addresses), 0),
2033
- COALESCE(addresses.whale_addresses, '{}'),
2034
- metrics.buy_notional_usd,
2035
- metrics.sell_notional_usd,
2036
- metrics.buy_notional_usd - metrics.sell_notional_usd,
2037
- CASE
2038
- WHEN metrics.buy_notional_usd + metrics.sell_notional_usd > 0
2039
- THEN metrics.buy_notional_usd /
2040
- (metrics.buy_notional_usd + metrics.sell_notional_usd)
2041
- ELSE NULL
2042
- END,
2043
- COALESCE(position_metrics.position_aware_whale_sides, 0),
2044
- COALESCE(position_metrics.long_entry_whale_addresses, '{}'),
2045
- COALESCE(position_metrics.short_entry_whale_addresses, '{}'),
2046
- COALESCE(position_metrics.long_exit_whale_addresses, '{}'),
2047
- COALESCE(position_metrics.short_exit_whale_addresses, '{}'),
2048
- COALESCE(position_metrics.long_entry_notional_usd, 0),
2049
- COALESCE(position_metrics.short_entry_notional_usd, 0),
2050
- COALESCE(position_metrics.long_exit_notional_usd, 0),
2051
- COALESCE(position_metrics.short_exit_notional_usd, 0),
2052
- COALESCE(position_metrics.long_entry_notional_usd, 0) -
2053
- COALESCE(position_metrics.short_entry_notional_usd, 0),
2054
- CASE
2055
- WHEN COALESCE(position_metrics.long_entry_notional_usd, 0) +
2056
- COALESCE(position_metrics.short_entry_notional_usd, 0) > 0
2057
- THEN COALESCE(position_metrics.long_entry_notional_usd, 0) /
2058
- (
2059
- COALESCE(position_metrics.long_entry_notional_usd, 0) +
2060
- COALESCE(position_metrics.short_entry_notional_usd, 0)
2061
- )
2062
- ELSE NULL
2063
- END,
2064
- $1,
2065
- $2,
2066
- CASE
2067
- WHEN COALESCE(position_metrics.position_aware_whale_sides, 0) > 0
2068
- THEN 'hyperliquid_user_fills'
2069
- ELSE 'hyperliquid_trades'
2070
- END
2071
- FROM metrics
2072
- LEFT JOIN addresses USING (symbol, bucket_ts)
2073
- LEFT JOIN position_metrics USING (symbol, bucket_ts)
2074
- ON CONFLICT (
2075
- universe_fingerprint,
2076
- whale_registry_fingerprint,
2077
- symbol,
2078
- interval,
2079
- ts
2080
- ) DO UPDATE SET
2081
- trades = EXCLUDED.trades,
2082
- whale_sides = EXCLUDED.whale_sides,
2083
- unique_whales = EXCLUDED.unique_whales,
2084
- whale_addresses = EXCLUDED.whale_addresses,
2085
- buy_notional_usd = EXCLUDED.buy_notional_usd,
2086
- sell_notional_usd = EXCLUDED.sell_notional_usd,
2087
- net_notional_usd = EXCLUDED.net_notional_usd,
2088
- buy_share_pct = EXCLUDED.buy_share_pct,
2089
- position_aware_whale_sides = EXCLUDED.position_aware_whale_sides,
2090
- long_entry_whale_addresses = EXCLUDED.long_entry_whale_addresses,
2091
- short_entry_whale_addresses = EXCLUDED.short_entry_whale_addresses,
2092
- long_exit_whale_addresses = EXCLUDED.long_exit_whale_addresses,
2093
- short_exit_whale_addresses = EXCLUDED.short_exit_whale_addresses,
2094
- long_entry_notional_usd = EXCLUDED.long_entry_notional_usd,
2095
- short_entry_notional_usd = EXCLUDED.short_entry_notional_usd,
2096
- long_exit_notional_usd = EXCLUDED.long_exit_notional_usd,
2097
- short_exit_notional_usd = EXCLUDED.short_exit_notional_usd,
2098
- entry_net_notional_usd = EXCLUDED.entry_net_notional_usd,
2099
- entry_long_share_pct = EXCLUDED.entry_long_share_pct,
2100
- source = EXCLUDED.source,
2101
- ingested_at = now()
2102
- RETURNING 1
2103
- `,
2104
- [
2105
- params.universeFingerprint,
2106
- params.whaleRegistryFingerprint,
2107
- params.fromMs,
2108
- params.toMs
2109
- ]
2110
- );
2111
- if (params.deleteEventsBeforeMs != null) {
2112
- await client.query(
2113
- `
2114
- DELETE FROM hyperliquid_whale_trade_events
2115
- WHERE universe_fingerprint = $1
2116
- AND whale_registry_fingerprint = $2
2117
- AND ts < to_timestamp($3/1000.0)
2118
- `,
2119
- [
2120
- params.universeFingerprint,
2121
- params.whaleRegistryFingerprint,
2122
- params.deleteEventsBeforeMs
2123
- ]
2124
- );
2125
- }
2126
- await client.query("COMMIT");
2127
- return result.rowCount ?? 0;
2128
- } catch (error) {
2129
- await client.query("ROLLBACK");
2130
- throw error;
2131
- } finally {
2132
- client.release();
2133
- }
2134
- }
2135
- async function upsertMarketBreadthRows(rows) {
2132
+ async function upsertMarketCmcExchangeLiquidityContextRows(rows) {
2136
2133
  if (!rows.length) return;
2137
2134
  await ensureBinanceMarketSchema();
2138
2135
  const pool = getPool();
2139
2136
  const cols = [
2140
- "universe",
2137
+ "source",
2141
2138
  "interval",
2142
2139
  "ts",
2143
- "symbols_count",
2144
- "advancers",
2145
- "decliners",
2146
- "unchanged",
2147
- "advance_decline_ratio",
2148
- "pct_above_ma20",
2149
- "pct_above_ma50",
2150
- "equal_weighted_return",
2151
- "volume_weighted_return",
2152
- "dispersion",
2153
- "btc_return_1h",
2154
- "btc_return_4h",
2155
- "btc_return_24h",
2156
- "alt_basket_return_1h",
2157
- "alt_basket_return_4h",
2158
- "alt_basket_return_24h",
2159
- "btc_vs_alt_return_1h",
2160
- "btc_vs_alt_return_4h",
2161
- "btc_vs_alt_return_24h",
2162
- "btc_turnover_share_1h",
2163
- "btc_turnover_share_24h",
2164
- "btc_turnover_share_change_24h",
2165
- "alt_vol_to_btc_vol_24h",
2166
- "alt_dispersion_24h",
2167
- "btc_alt_regime",
2168
- "source"
2140
+ "exchanges_count",
2141
+ "total_volume_usd",
2142
+ "binance_volume_usd",
2143
+ "binance_volume_share",
2144
+ "top_exchange_volume_share",
2145
+ "liquidity_regime"
2169
2146
  ];
2170
2147
  const maxRows = getSafeBulkInsertRows(cols.length);
2171
2148
  if (rows.length > maxRows) {
2172
2149
  for (let i = 0; i < rows.length; i += maxRows) {
2173
- await upsertMarketBreadthRows(rows.slice(i, i + maxRows));
2150
+ await upsertMarketCmcExchangeLiquidityContextRows(
2151
+ rows.slice(i, i + maxRows)
2152
+ );
2174
2153
  }
2175
2154
  return;
2176
2155
  }
@@ -2178,235 +2157,7 @@ async function upsertMarketBreadthRows(rows) {
2178
2157
  (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2179
2158
  ).join(",");
2180
2159
  const flat = rows.flatMap((row) => [
2181
- row.universe,
2182
- row.interval,
2183
- row.ts,
2184
- row.symbolsCount,
2185
- row.advancers,
2186
- row.decliners,
2187
- row.unchanged,
2188
- row.advanceDeclineRatio ?? null,
2189
- row.pctAboveMa20 ?? null,
2190
- row.pctAboveMa50 ?? null,
2191
- row.equalWeightedReturn ?? null,
2192
- row.volumeWeightedReturn ?? null,
2193
- row.dispersion ?? null,
2194
- row.btcReturn1h ?? null,
2195
- row.btcReturn4h ?? null,
2196
- row.btcReturn24h ?? null,
2197
- row.altBasketReturn1h ?? null,
2198
- row.altBasketReturn4h ?? null,
2199
- row.altBasketReturn24h ?? null,
2200
- row.btcVsAltReturn1h ?? null,
2201
- row.btcVsAltReturn4h ?? null,
2202
- row.btcVsAltReturn24h ?? null,
2203
- row.btcTurnoverShare1h ?? null,
2204
- row.btcTurnoverShare24h ?? null,
2205
- row.btcTurnoverShareChange24h ?? null,
2206
- row.altVolToBtcVol24h ?? null,
2207
- row.altDispersion24h ?? null,
2208
- row.btcAltRegime ?? null,
2209
- row.source ?? null
2210
- ]);
2211
- await pool.query(
2212
- `
2213
- INSERT INTO market_breadth (${cols.join(",")})
2214
- VALUES ${valuesSql}
2215
- ON CONFLICT (universe, interval, ts) DO UPDATE SET
2216
- symbols_count = EXCLUDED.symbols_count,
2217
- advancers = EXCLUDED.advancers,
2218
- decliners = EXCLUDED.decliners,
2219
- unchanged = EXCLUDED.unchanged,
2220
- advance_decline_ratio = COALESCE(EXCLUDED.advance_decline_ratio, market_breadth.advance_decline_ratio),
2221
- pct_above_ma20 = COALESCE(EXCLUDED.pct_above_ma20, market_breadth.pct_above_ma20),
2222
- pct_above_ma50 = COALESCE(EXCLUDED.pct_above_ma50, market_breadth.pct_above_ma50),
2223
- equal_weighted_return = COALESCE(EXCLUDED.equal_weighted_return, market_breadth.equal_weighted_return),
2224
- volume_weighted_return = COALESCE(EXCLUDED.volume_weighted_return, market_breadth.volume_weighted_return),
2225
- dispersion = COALESCE(EXCLUDED.dispersion, market_breadth.dispersion),
2226
- btc_return_1h = COALESCE(EXCLUDED.btc_return_1h, market_breadth.btc_return_1h),
2227
- btc_return_4h = COALESCE(EXCLUDED.btc_return_4h, market_breadth.btc_return_4h),
2228
- btc_return_24h = COALESCE(EXCLUDED.btc_return_24h, market_breadth.btc_return_24h),
2229
- alt_basket_return_1h = COALESCE(EXCLUDED.alt_basket_return_1h, market_breadth.alt_basket_return_1h),
2230
- alt_basket_return_4h = COALESCE(EXCLUDED.alt_basket_return_4h, market_breadth.alt_basket_return_4h),
2231
- alt_basket_return_24h = COALESCE(EXCLUDED.alt_basket_return_24h, market_breadth.alt_basket_return_24h),
2232
- btc_vs_alt_return_1h = COALESCE(EXCLUDED.btc_vs_alt_return_1h, market_breadth.btc_vs_alt_return_1h),
2233
- btc_vs_alt_return_4h = COALESCE(EXCLUDED.btc_vs_alt_return_4h, market_breadth.btc_vs_alt_return_4h),
2234
- btc_vs_alt_return_24h = COALESCE(EXCLUDED.btc_vs_alt_return_24h, market_breadth.btc_vs_alt_return_24h),
2235
- btc_turnover_share_1h = COALESCE(EXCLUDED.btc_turnover_share_1h, market_breadth.btc_turnover_share_1h),
2236
- btc_turnover_share_24h = COALESCE(EXCLUDED.btc_turnover_share_24h, market_breadth.btc_turnover_share_24h),
2237
- btc_turnover_share_change_24h = COALESCE(EXCLUDED.btc_turnover_share_change_24h, market_breadth.btc_turnover_share_change_24h),
2238
- alt_vol_to_btc_vol_24h = COALESCE(EXCLUDED.alt_vol_to_btc_vol_24h, market_breadth.alt_vol_to_btc_vol_24h),
2239
- alt_dispersion_24h = COALESCE(EXCLUDED.alt_dispersion_24h, market_breadth.alt_dispersion_24h),
2240
- btc_alt_regime = COALESCE(EXCLUDED.btc_alt_regime, market_breadth.btc_alt_regime),
2241
- source = COALESCE(EXCLUDED.source, market_breadth.source),
2242
- ingested_at = now()
2243
- `,
2244
- flat
2245
- );
2246
- }
2247
- async function upsertMarketGlobalContextRows(rows) {
2248
- if (!rows.length) return;
2249
- await ensureBinanceMarketSchema();
2250
- const pool = getPool();
2251
- const cols = [
2252
- "source",
2253
- "ts",
2254
- "updated_at_ts",
2255
- "active_cryptocurrencies",
2256
- "active_exchanges",
2257
- "active_market_pairs",
2258
- "markets",
2259
- "total_market_cap_usd",
2260
- "total_volume_usd",
2261
- "total_volume_reported_usd",
2262
- "btc_dominance_pct",
2263
- "eth_dominance_pct",
2264
- "alt_market_cap_usd",
2265
- "alt_volume_usd",
2266
- "alt_volume_reported_usd",
2267
- "btc_to_alt_market_cap_ratio",
2268
- "market_cap_change_pct_24h_usd"
2269
- ];
2270
- const maxRows = getSafeBulkInsertRows(cols.length);
2271
- if (rows.length > maxRows) {
2272
- for (let i = 0; i < rows.length; i += maxRows) {
2273
- await upsertMarketGlobalContextRows(rows.slice(i, i + maxRows));
2274
- }
2275
- return;
2276
- }
2277
- const valuesSql = rows.map(
2278
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2279
- ).join(",");
2280
- const flat = rows.flatMap((row) => [
2281
- row.source,
2282
- row.ts,
2283
- row.updatedAt ?? null,
2284
- row.activeCryptocurrencies ?? null,
2285
- row.activeExchanges ?? null,
2286
- row.activeMarketPairs ?? null,
2287
- row.markets ?? null,
2288
- row.totalMarketCapUsd ?? null,
2289
- row.totalVolumeUsd ?? null,
2290
- row.totalVolumeReportedUsd ?? null,
2291
- row.btcDominancePct ?? null,
2292
- row.ethDominancePct ?? null,
2293
- row.altMarketCapUsd ?? null,
2294
- row.altVolumeUsd ?? null,
2295
- row.altVolumeReportedUsd ?? null,
2296
- row.btcToAltMarketCapRatio ?? null,
2297
- row.marketCapChangePct24hUsd ?? null
2298
- ]);
2299
- await pool.query(
2300
- `
2301
- INSERT INTO market_global_context (${cols.join(",")})
2302
- VALUES ${valuesSql}
2303
- ON CONFLICT (source, ts) DO UPDATE SET
2304
- updated_at_ts = COALESCE(EXCLUDED.updated_at_ts, market_global_context.updated_at_ts),
2305
- active_cryptocurrencies = COALESCE(EXCLUDED.active_cryptocurrencies, market_global_context.active_cryptocurrencies),
2306
- active_exchanges = COALESCE(EXCLUDED.active_exchanges, market_global_context.active_exchanges),
2307
- active_market_pairs = COALESCE(EXCLUDED.active_market_pairs, market_global_context.active_market_pairs),
2308
- markets = COALESCE(EXCLUDED.markets, market_global_context.markets),
2309
- total_market_cap_usd = COALESCE(EXCLUDED.total_market_cap_usd, market_global_context.total_market_cap_usd),
2310
- total_volume_usd = COALESCE(EXCLUDED.total_volume_usd, market_global_context.total_volume_usd),
2311
- total_volume_reported_usd = COALESCE(EXCLUDED.total_volume_reported_usd, market_global_context.total_volume_reported_usd),
2312
- btc_dominance_pct = COALESCE(EXCLUDED.btc_dominance_pct, market_global_context.btc_dominance_pct),
2313
- eth_dominance_pct = COALESCE(EXCLUDED.eth_dominance_pct, market_global_context.eth_dominance_pct),
2314
- alt_market_cap_usd = COALESCE(EXCLUDED.alt_market_cap_usd, market_global_context.alt_market_cap_usd),
2315
- alt_volume_usd = COALESCE(EXCLUDED.alt_volume_usd, market_global_context.alt_volume_usd),
2316
- alt_volume_reported_usd = COALESCE(EXCLUDED.alt_volume_reported_usd, market_global_context.alt_volume_reported_usd),
2317
- btc_to_alt_market_cap_ratio = COALESCE(EXCLUDED.btc_to_alt_market_cap_ratio, market_global_context.btc_to_alt_market_cap_ratio),
2318
- market_cap_change_pct_24h_usd = COALESCE(EXCLUDED.market_cap_change_pct_24h_usd, market_global_context.market_cap_change_pct_24h_usd),
2319
- ingested_at = now()
2320
- `,
2321
- flat
2322
- );
2323
- }
2324
- async function upsertMarketReferenceAssetContextRows(rows) {
2325
- if (!rows.length) return;
2326
- await ensureBinanceMarketSchema();
2327
- const pool = getPool();
2328
- const cols = [
2329
- "source",
2330
- "symbol",
2331
- "cmc_id",
2332
- "interval",
2333
- "ts",
2334
- "open_usd",
2335
- "high_usd",
2336
- "low_usd",
2337
- "close_usd",
2338
- "volume_usd",
2339
- "market_cap_usd"
2340
- ];
2341
- const maxRows = getSafeBulkInsertRows(cols.length);
2342
- if (rows.length > maxRows) {
2343
- for (let i = 0; i < rows.length; i += maxRows) {
2344
- await upsertMarketReferenceAssetContextRows(rows.slice(i, i + maxRows));
2345
- }
2346
- return;
2347
- }
2348
- const valuesSql = rows.map(
2349
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2350
- ).join(",");
2351
- const flat = rows.flatMap((row) => [
2352
- row.source,
2353
- row.symbol.trim().toUpperCase(),
2354
- Math.trunc(row.cmcId),
2355
- row.interval,
2356
- row.ts,
2357
- row.openUsd ?? null,
2358
- row.highUsd ?? null,
2359
- row.lowUsd ?? null,
2360
- row.closeUsd ?? null,
2361
- row.volumeUsd ?? null,
2362
- row.marketCapUsd ?? null
2363
- ]);
2364
- await pool.query(
2365
- `
2366
- INSERT INTO market_reference_asset_context (${cols.join(",")})
2367
- VALUES ${valuesSql}
2368
- ON CONFLICT (source, symbol, interval, ts) DO UPDATE SET
2369
- cmc_id = EXCLUDED.cmc_id,
2370
- open_usd = COALESCE(EXCLUDED.open_usd, market_reference_asset_context.open_usd),
2371
- high_usd = COALESCE(EXCLUDED.high_usd, market_reference_asset_context.high_usd),
2372
- low_usd = COALESCE(EXCLUDED.low_usd, market_reference_asset_context.low_usd),
2373
- close_usd = COALESCE(EXCLUDED.close_usd, market_reference_asset_context.close_usd),
2374
- volume_usd = COALESCE(EXCLUDED.volume_usd, market_reference_asset_context.volume_usd),
2375
- market_cap_usd = COALESCE(EXCLUDED.market_cap_usd, market_reference_asset_context.market_cap_usd),
2376
- ingested_at = now()
2377
- `,
2378
- flat
2379
- );
2380
- }
2381
- async function upsertMarketCmcExchangeLiquidityContextRows(rows) {
2382
- if (!rows.length) return;
2383
- await ensureBinanceMarketSchema();
2384
- const pool = getPool();
2385
- const cols = [
2386
- "source",
2387
- "interval",
2388
- "ts",
2389
- "exchanges_count",
2390
- "total_volume_usd",
2391
- "binance_volume_usd",
2392
- "binance_volume_share",
2393
- "top_exchange_volume_share",
2394
- "liquidity_regime"
2395
- ];
2396
- const maxRows = getSafeBulkInsertRows(cols.length);
2397
- if (rows.length > maxRows) {
2398
- for (let i = 0; i < rows.length; i += maxRows) {
2399
- await upsertMarketCmcExchangeLiquidityContextRows(
2400
- rows.slice(i, i + maxRows)
2401
- );
2402
- }
2403
- return;
2404
- }
2405
- const valuesSql = rows.map(
2406
- (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
2407
- ).join(",");
2408
- const flat = rows.flatMap((row) => [
2409
- row.source,
2160
+ row.source,
2410
2161
  row.interval,
2411
2162
  row.ts,
2412
2163
  Math.trunc(row.exchangesCount),
@@ -2615,10 +2366,6 @@ async function upsertMarketContextBackfillCoverage(rows) {
2615
2366
  flat
2616
2367
  );
2617
2368
  }
2618
- var toMarketFeatureAge = (rowTs, atMs) => {
2619
- const ageMs = atMs - rowTs.getTime();
2620
- return Number.isFinite(ageMs) ? ageMs : null;
2621
- };
2622
2369
  async function getLatestMarketTradeFlow(params) {
2623
2370
  await prepareMarketContextSchemaForRead("binance");
2624
2371
  const res = await queryMarketContext(
@@ -2655,695 +2402,388 @@ async function getLatestMarketTradeFlow(params) {
2655
2402
  stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
2656
2403
  };
2657
2404
  }
2658
- var HYPERLIQUID_CONTEXT_INTERVAL_MS = {
2659
- "1m": 6e4,
2660
- "5m": 5 * 6e4,
2661
- "15m": 15 * 6e4,
2662
- "1h": 60 * 6e4
2663
- };
2664
- async function getHyperliquidWhaleWalletCoverage(params) {
2665
- await ensureHyperliquidWhaleSchema();
2666
- const result = await getPool().query(
2405
+ async function getLatestMarketBreadth(params) {
2406
+ await prepareMarketContextSchemaForRead("binance");
2407
+ const res = await queryMarketContext(
2667
2408
  `
2668
2409
  SELECT
2669
- status,
2670
- covered_from_ts,
2671
- covered_to_ts,
2672
- fills_count,
2673
- error,
2674
- checked_at
2675
- FROM hyperliquid_whale_wallet_coverage
2676
- WHERE universe_fingerprint = $1
2677
- AND whale_registry_fingerprint = $2
2678
- AND address = $3
2679
- AND data_model_version = $6
2680
- AND (
2681
- (
2682
- covered_from_ts <= to_timestamp($4/1000.0)
2683
- AND covered_to_ts >= to_timestamp($5/1000.0)
2684
- )
2685
- OR (
2686
- requested_from_ts = to_timestamp($4/1000.0)
2687
- AND requested_to_ts = to_timestamp($5/1000.0)
2688
- )
2689
- )
2690
- ORDER BY checked_at DESC
2410
+ universe,
2411
+ interval,
2412
+ ts,
2413
+ symbols_count::int AS "symbolsCount",
2414
+ advancers::int AS advancers,
2415
+ decliners::int AS decliners,
2416
+ unchanged::int AS unchanged,
2417
+ advance_decline_ratio AS "advanceDeclineRatio",
2418
+ pct_above_ma20 AS "pctAboveMa20",
2419
+ pct_above_ma50 AS "pctAboveMa50",
2420
+ equal_weighted_return AS "equalWeightedReturn",
2421
+ volume_weighted_return AS "volumeWeightedReturn",
2422
+ dispersion,
2423
+ btc_return_1h AS "btcReturn1h",
2424
+ btc_return_4h AS "btcReturn4h",
2425
+ btc_return_24h AS "btcReturn24h",
2426
+ alt_basket_return_1h AS "altBasketReturn1h",
2427
+ alt_basket_return_4h AS "altBasketReturn4h",
2428
+ alt_basket_return_24h AS "altBasketReturn24h",
2429
+ btc_vs_alt_return_1h AS "btcVsAltReturn1h",
2430
+ btc_vs_alt_return_4h AS "btcVsAltReturn4h",
2431
+ btc_vs_alt_return_24h AS "btcVsAltReturn24h",
2432
+ btc_turnover_share_1h AS "btcTurnoverShare1h",
2433
+ btc_turnover_share_24h AS "btcTurnoverShare24h",
2434
+ btc_turnover_share_change_24h AS "btcTurnoverShareChange24h",
2435
+ alt_vol_to_btc_vol_24h AS "altVolToBtcVol24h",
2436
+ alt_dispersion_24h AS "altDispersion24h",
2437
+ btc_alt_regime AS "btcAltRegime",
2438
+ source
2439
+ FROM market_breadth
2440
+ WHERE universe = $1
2441
+ AND interval = $2
2442
+ AND ts <= to_timestamp($3/1000.0)
2443
+ ORDER BY ts DESC
2691
2444
  LIMIT 1
2692
2445
  `,
2693
- [
2694
- params.universeFingerprint,
2695
- params.whaleRegistryFingerprint,
2696
- params.address.toLowerCase(),
2697
- params.fromMs,
2698
- params.toMs,
2699
- import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2700
- ]
2446
+ [params.universe, params.interval, params.atMs],
2447
+ params
2701
2448
  );
2702
- const row = result.rows[0];
2449
+ const row = res.rows[0];
2703
2450
  if (!row) return null;
2451
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
2704
2452
  return {
2705
- status: row.covered_from_ts != null && row.covered_to_ts != null && new Date(row.covered_from_ts).getTime() <= params.fromMs && new Date(row.covered_to_ts).getTime() >= params.toMs ? "complete" : String(row.status),
2706
- coveredFromMs: row.covered_from_ts == null ? null : new Date(row.covered_from_ts).getTime(),
2707
- coveredToMs: row.covered_to_ts == null ? null : new Date(row.covered_to_ts).getTime(),
2708
- fillsCount: Number(row.fills_count) || 0,
2709
- error: row.error == null ? null : String(row.error),
2710
- checkedAt: new Date(row.checked_at)
2453
+ ...row,
2454
+ ageMs,
2455
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
2711
2456
  };
2712
2457
  }
2713
- async function upsertHyperliquidWhaleWalletCoverage(params) {
2714
- await ensureHyperliquidWhaleSchema();
2715
- await getPool().query(
2458
+ async function getLatestMarketGlobalContext(params) {
2459
+ await prepareMarketContextSchemaForRead("coinmarketcap");
2460
+ const source = params.source ?? "coinmarketcap_global";
2461
+ const res = await queryMarketContext(
2716
2462
  `
2717
- INSERT INTO hyperliquid_whale_wallet_coverage (
2718
- universe_fingerprint,
2719
- whale_registry_fingerprint,
2720
- address,
2721
- requested_from_ts,
2722
- requested_to_ts,
2723
- covered_from_ts,
2724
- covered_to_ts,
2725
- status,
2726
- fills_count,
2727
- error,
2728
- data_model_version
2729
- ) VALUES (
2730
- $1,
2731
- $2,
2732
- $3,
2733
- to_timestamp($4/1000.0),
2734
- to_timestamp($5/1000.0),
2735
- CASE WHEN $6::double precision IS NULL THEN NULL ELSE to_timestamp($6/1000.0) END,
2736
- CASE WHEN $7::double precision IS NULL THEN NULL ELSE to_timestamp($7/1000.0) END,
2737
- $8,
2738
- $9,
2739
- $10,
2740
- $11
2741
- )
2742
- ON CONFLICT (
2743
- universe_fingerprint,
2744
- whale_registry_fingerprint,
2745
- address,
2746
- requested_from_ts,
2747
- requested_to_ts
2748
- ) DO UPDATE SET
2749
- covered_from_ts = EXCLUDED.covered_from_ts,
2750
- covered_to_ts = EXCLUDED.covered_to_ts,
2751
- status = EXCLUDED.status,
2752
- fills_count = EXCLUDED.fills_count,
2753
- error = EXCLUDED.error,
2754
- data_model_version = EXCLUDED.data_model_version,
2755
- checked_at = now()
2463
+ SELECT
2464
+ source,
2465
+ ts,
2466
+ updated_at_ts AS "updatedAt",
2467
+ active_cryptocurrencies::int AS "activeCryptocurrencies",
2468
+ active_exchanges::int AS "activeExchanges",
2469
+ active_market_pairs::int AS "activeMarketPairs",
2470
+ markets::int AS markets,
2471
+ total_market_cap_usd AS "totalMarketCapUsd",
2472
+ total_volume_usd AS "totalVolumeUsd",
2473
+ total_volume_reported_usd AS "totalVolumeReportedUsd",
2474
+ btc_dominance_pct AS "btcDominancePct",
2475
+ eth_dominance_pct AS "ethDominancePct",
2476
+ alt_market_cap_usd AS "altMarketCapUsd",
2477
+ alt_volume_usd AS "altVolumeUsd",
2478
+ alt_volume_reported_usd AS "altVolumeReportedUsd",
2479
+ btc_to_alt_market_cap_ratio AS "btcToAltMarketCapRatio",
2480
+ market_cap_change_pct_24h_usd AS "marketCapChangePct24hUsd"
2481
+ FROM market_global_context
2482
+ WHERE source = $1
2483
+ AND ts + CASE
2484
+ WHEN source = 'coinmarketcap_global' THEN interval '1 day'
2485
+ ELSE interval '0 seconds'
2486
+ END <= to_timestamp($2/1000.0)
2487
+ ORDER BY ts DESC
2488
+ LIMIT 1
2756
2489
  `,
2757
- [
2758
- params.universeFingerprint,
2759
- params.whaleRegistryFingerprint,
2760
- params.address.toLowerCase(),
2761
- params.fromMs,
2762
- params.toMs,
2763
- params.coveredFromMs,
2764
- params.coveredToMs,
2765
- params.status,
2766
- params.fillsCount,
2767
- params.error ?? null,
2768
- import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2769
- ]
2490
+ [source, params.atMs],
2491
+ params
2492
+ );
2493
+ const row = res.rows[0];
2494
+ if (!row) return null;
2495
+ const previousRes = await queryMarketContext(
2496
+ `
2497
+ SELECT
2498
+ btc_dominance_pct AS "btcDominancePct",
2499
+ eth_dominance_pct AS "ethDominancePct",
2500
+ alt_market_cap_usd AS "altMarketCapUsd",
2501
+ alt_volume_usd AS "altVolumeUsd"
2502
+ FROM market_global_context
2503
+ WHERE source = $1
2504
+ AND ts <= $2::timestamptz - interval '24 hours'
2505
+ ORDER BY ts DESC
2506
+ LIMIT 1
2507
+ `,
2508
+ [source, row.ts],
2509
+ params
2770
2510
  );
2511
+ const previousDominance = previousRes.rows[0]?.btcDominancePct == null ? null : Number(previousRes.rows[0].btcDominancePct);
2512
+ const previousEthDominance = previousRes.rows[0]?.ethDominancePct == null ? null : Number(previousRes.rows[0].ethDominancePct);
2513
+ const previousAltMarketCap = previousRes.rows[0]?.altMarketCapUsd == null ? null : Number(previousRes.rows[0].altMarketCapUsd);
2514
+ const previousAltVolume = previousRes.rows[0]?.altVolumeUsd == null ? null : Number(previousRes.rows[0].altVolumeUsd);
2515
+ const currentDominance = row.btcDominancePct == null ? null : Number(row.btcDominancePct);
2516
+ const currentEthDominance = row.ethDominancePct == null ? null : Number(row.ethDominancePct);
2517
+ const currentAltMarketCap = row.altMarketCapUsd == null ? null : Number(row.altMarketCapUsd);
2518
+ const currentAltVolume = row.altVolumeUsd == null ? null : Number(row.altVolumeUsd);
2519
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
2520
+ return {
2521
+ ...row,
2522
+ ageMs,
2523
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
2524
+ btcDominanceChange24hPct: currentDominance != null && previousDominance != null ? currentDominance - previousDominance : null,
2525
+ ethDominanceChange24hPct: currentEthDominance != null && previousEthDominance != null ? currentEthDominance - previousEthDominance : null,
2526
+ altMarketCapChange24hPct: currentAltMarketCap != null && previousAltMarketCap != null && previousAltMarketCap > 0 ? (currentAltMarketCap - previousAltMarketCap) / previousAltMarketCap : null,
2527
+ altVolumeChange24hPct: currentAltVolume != null && previousAltVolume != null && previousAltVolume > 0 ? (currentAltVolume - previousAltVolume) / previousAltVolume : null
2528
+ };
2771
2529
  }
2772
- async function rebuildHyperliquidWhaleCoverageRows(params) {
2773
- await ensureHyperliquidWhaleSchema();
2774
- if (params.toMs <= params.fromMs) return 0;
2775
- const minuteMs = 6e4;
2776
- const defaultChunkMinutes = 7 * 24 * 60;
2777
- const chunkMinutes = Number.isFinite(params.chunkMinutes) && Number(params.chunkMinutes) > 0 ? Math.floor(Number(params.chunkMinutes)) : defaultChunkMinutes;
2778
- const chunkMs = chunkMinutes * minuteMs;
2779
- const totalBuckets = Math.ceil((params.toMs - params.fromMs) / minuteMs);
2780
- const totalChunks = Math.ceil((params.toMs - params.fromMs) / chunkMs);
2781
- let completedBuckets = 0;
2782
- let rows = 0;
2783
- for (let chunkIndex = 0, chunkFromMs = params.fromMs; chunkFromMs < params.toMs; chunkIndex += 1, chunkFromMs += chunkMs) {
2784
- const chunkToMs = Math.min(params.toMs, chunkFromMs + chunkMs);
2785
- const result = await getPool().query(
2786
- `
2787
- WITH normalized_ranges AS (
2788
- SELECT
2789
- address,
2790
- GREATEST(
2791
- to_timestamp($3/1000.0),
2792
- date_trunc('minute', covered_from_ts) +
2793
- CASE
2794
- WHEN covered_from_ts = date_trunc('minute', covered_from_ts)
2795
- THEN interval '0 minutes'
2796
- ELSE interval '1 minute'
2797
- END
2798
- ) AS range_start,
2799
- LEAST(
2800
- to_timestamp($4/1000.0),
2801
- date_trunc('minute', covered_to_ts)
2802
- ) AS range_end
2803
- FROM hyperliquid_whale_wallet_coverage
2804
- WHERE universe_fingerprint = $1
2805
- AND whale_registry_fingerprint = $2
2806
- AND data_model_version = $6
2807
- AND status IN ('complete', 'truncated')
2808
- AND covered_from_ts < to_timestamp($4/1000.0)
2809
- AND covered_to_ts > to_timestamp($3/1000.0)
2810
- ), eligible_ranges AS (
2811
- SELECT *
2812
- FROM normalized_ranges
2813
- WHERE range_start < range_end
2814
- ), ordered_ranges AS (
2815
- SELECT
2816
- *,
2817
- MAX(range_end) OVER (
2818
- PARTITION BY address
2819
- ORDER BY range_start, range_end
2820
- ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
2821
- ) AS previous_max_end
2822
- FROM eligible_ranges
2823
- ), marked_ranges AS (
2824
- SELECT
2825
- *,
2826
- SUM(
2827
- CASE
2828
- WHEN previous_max_end IS NULL OR range_start > previous_max_end
2829
- THEN 1
2830
- ELSE 0
2831
- END
2832
- ) OVER (
2833
- PARTITION BY address
2834
- ORDER BY range_start, range_end
2835
- ) AS range_group
2836
- FROM ordered_ranges
2837
- ), merged_ranges AS (
2838
- SELECT
2839
- address,
2840
- MIN(range_start) AS range_start,
2841
- MAX(range_end) AS range_end
2842
- FROM marked_ranges
2843
- GROUP BY address, range_group
2844
- ), deltas AS (
2845
- SELECT range_start AS ts, 1 AS delta
2846
- FROM merged_ranges
2847
- UNION ALL
2848
- SELECT range_end AS ts, -1 AS delta
2849
- FROM merged_ranges
2850
- ), bucket_deltas AS (
2851
- SELECT ts, SUM(delta)::int AS delta
2852
- FROM deltas
2853
- GROUP BY ts
2854
- ), buckets AS (
2855
- SELECT generate_series(
2856
- to_timestamp($3/1000.0),
2857
- to_timestamp($4/1000.0) - interval '1 minute',
2858
- interval '1 minute'
2859
- ) AS ts
2860
- ), coverage AS (
2861
- SELECT
2862
- buckets.ts,
2863
- SUM(COALESCE(bucket_deltas.delta, 0)) OVER (
2864
- ORDER BY buckets.ts
2865
- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
2866
- )::int AS covered_whales
2867
- FROM buckets
2868
- LEFT JOIN bucket_deltas USING (ts)
2869
- )
2870
- INSERT INTO hyperliquid_whale_coverage_1m (
2871
- ts,
2872
- covered_whales,
2873
- expected_whales,
2874
- coverage_pct,
2875
- universe_fingerprint,
2876
- whale_registry_fingerprint,
2877
- source,
2878
- data_model_version
2879
- )
2880
- SELECT
2881
- ts,
2882
- covered_whales,
2883
- $5,
2884
- CASE WHEN $5 > 0 THEN covered_whales::double precision / $5 ELSE 0 END,
2885
- $1,
2886
- $2,
2887
- 'hyperliquid_user_fills',
2888
- $6
2889
- FROM coverage
2890
- ON CONFLICT (
2891
- universe_fingerprint,
2892
- whale_registry_fingerprint,
2893
- ts
2894
- ) DO UPDATE SET
2895
- covered_whales = EXCLUDED.covered_whales,
2896
- expected_whales = EXCLUDED.expected_whales,
2897
- coverage_pct = EXCLUDED.coverage_pct,
2898
- source = EXCLUDED.source,
2899
- data_model_version = EXCLUDED.data_model_version,
2900
- ingested_at = now()
2901
- `,
2902
- [
2903
- params.universeFingerprint,
2904
- params.whaleRegistryFingerprint,
2905
- chunkFromMs,
2906
- chunkToMs,
2907
- params.expectedWhales,
2908
- import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2909
- ]
2910
- );
2911
- const chunkBuckets = Math.ceil((chunkToMs - chunkFromMs) / minuteMs);
2912
- completedBuckets = Math.min(totalBuckets, completedBuckets + chunkBuckets);
2913
- rows += result.rowCount ?? 0;
2914
- params.onProgress?.({
2915
- chunkIndex: chunkIndex + 1,
2916
- totalChunks,
2917
- completedBuckets,
2918
- totalBuckets,
2919
- rows
2920
- });
2921
- }
2922
- return rows;
2923
- }
2924
- async function upsertHyperliquidWhaleCoverageRows(rows) {
2925
- if (!rows.length) return;
2926
- await ensureHyperliquidWhaleSchema();
2927
- const cols = [
2928
- "ts",
2929
- "covered_whales",
2930
- "expected_whales",
2931
- "coverage_pct",
2932
- "universe_fingerprint",
2933
- "whale_registry_fingerprint",
2934
- "source",
2935
- "data_model_version"
2936
- ];
2937
- const maxRows = Math.floor(PG_SAFE_MAX_BIND_PARAMS / cols.length);
2938
- if (rows.length > maxRows) {
2939
- for (let index = 0; index < rows.length; index += maxRows) {
2940
- await upsertHyperliquidWhaleCoverageRows(
2941
- rows.slice(index, index + maxRows)
2942
- );
2943
- }
2944
- return;
2945
- }
2946
- const values = [];
2947
- const tuples = rows.map((row, rowIndex) => {
2948
- const offset = rowIndex * cols.length;
2949
- values.push(
2950
- row.ts,
2951
- row.coveredWhales,
2952
- row.expectedWhales,
2953
- row.coveragePct,
2954
- row.universeFingerprint,
2955
- row.whaleRegistryFingerprint,
2956
- row.source ?? null,
2957
- row.dataModelVersion ?? import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
2958
- );
2959
- return `(${cols.map((_, colIndex) => `$${offset + colIndex + 1}`).join(",")})`;
2960
- });
2961
- await getPool().query(
2530
+ async function getMarketGlobalContextCoverage(params) {
2531
+ await ensureBinanceMarketSchema();
2532
+ const pool = getPool();
2533
+ const res = await pool.query(
2962
2534
  `
2963
- INSERT INTO hyperliquid_whale_coverage_1m (${cols.join(",")})
2964
- VALUES ${tuples.join(",")}
2965
- ON CONFLICT (
2966
- universe_fingerprint,
2967
- whale_registry_fingerprint,
2968
- ts
2969
- ) DO UPDATE SET
2970
- covered_whales = EXCLUDED.covered_whales,
2971
- expected_whales = EXCLUDED.expected_whales,
2972
- coverage_pct = EXCLUDED.coverage_pct,
2973
- source = EXCLUDED.source,
2974
- data_model_version = EXCLUDED.data_model_version,
2975
- ingested_at = now()
2535
+ SELECT
2536
+ extract(epoch from MIN(ts))*1000 AS first_ms,
2537
+ extract(epoch from MAX(ts))*1000 AS last_ms,
2538
+ COUNT(*)::int AS rows
2539
+ FROM market_global_context
2540
+ WHERE source = $1
2541
+ AND ts >= to_timestamp($2/1000.0)
2542
+ AND ts <= to_timestamp($3/1000.0)
2976
2543
  `,
2977
- values
2544
+ [params.source, params.startMs, params.endMs]
2978
2545
  );
2546
+ const row = res.rows[0];
2547
+ const rows = Number(row?.rows ?? 0);
2548
+ const firstMs = Number(row?.first_ms);
2549
+ const lastMs = Number(row?.last_ms);
2550
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
2551
+ return null;
2552
+ }
2553
+ return { firstMs, lastMs, rows };
2979
2554
  }
2980
- async function getHyperliquidWhaleCoverageSeriesRows(params) {
2981
- await prepareMarketContextSchemaForRead("hyperliquidWhales");
2982
- const result = await queryMarketContext(
2555
+ async function getMarketReferenceAssetContextCoverage(params) {
2556
+ const symbols = [
2557
+ ...new Set(
2558
+ params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
2559
+ )
2560
+ ];
2561
+ const coverage = /* @__PURE__ */ new Map();
2562
+ if (!symbols.length) return coverage;
2563
+ await ensureBinanceMarketSchema();
2564
+ const pool = getPool();
2565
+ const res = await pool.query(
2983
2566
  `
2984
2567
  SELECT
2985
- ts,
2986
- covered_whales,
2987
- expected_whales,
2988
- coverage_pct
2989
- FROM hyperliquid_whale_coverage_1m
2990
- WHERE universe_fingerprint = $1
2991
- AND whale_registry_fingerprint = $2
2992
- AND data_model_version = $3
2568
+ symbol,
2569
+ extract(epoch from MIN(ts))*1000 AS first_ms,
2570
+ extract(epoch from MAX(ts))*1000 AS last_ms,
2571
+ COUNT(*)::int AS rows
2572
+ FROM market_reference_asset_context
2573
+ WHERE source = $1
2574
+ AND symbol = ANY($2)
2575
+ AND interval = $3
2993
2576
  AND ts >= to_timestamp($4/1000.0)
2994
- AND ts < to_timestamp($5/1000.0)
2995
- ORDER BY ts
2577
+ AND ts <= to_timestamp($5/1000.0)
2578
+ GROUP BY symbol
2996
2579
  `,
2997
- [
2998
- params.universeFingerprint,
2999
- params.whaleRegistryFingerprint,
3000
- import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION,
3001
- params.fromMs,
3002
- params.toMs
3003
- ],
3004
- params
2580
+ [params.source, symbols, params.interval, params.startMs, params.endMs]
3005
2581
  );
3006
- return result.rows.map((row) => ({
3007
- ts: new Date(row.ts),
3008
- coveredWhales: Number(row.covered_whales) || 0,
3009
- expectedWhales: Number(row.expected_whales) || 0,
3010
- coveragePct: Number(row.coverage_pct) || 0
3011
- }));
2582
+ for (const row of res.rows) {
2583
+ const firstMs = Number(row.first_ms);
2584
+ const lastMs = Number(row.last_ms);
2585
+ const rows = Number(row.rows);
2586
+ if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
2587
+ coverage.set(row.symbol.toUpperCase(), { firstMs, lastMs, rows });
2588
+ }
2589
+ }
2590
+ return coverage;
3012
2591
  }
3013
- async function getHyperliquidWhaleFlowSeriesRows(params) {
3014
- await prepareMarketContextSchemaForRead("hyperliquidWhales");
3015
- const result = await queryMarketContext(
2592
+ async function getLatestMarketReferenceAssetContexts(params) {
2593
+ const source = params.source ?? "coinmarketcap_reference_asset";
2594
+ const interval = params.interval ?? "1d";
2595
+ const symbols = [
2596
+ ...new Set(
2597
+ params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
2598
+ )
2599
+ ];
2600
+ const rows = /* @__PURE__ */ new Map();
2601
+ if (!symbols.length) return rows;
2602
+ await prepareMarketContextSchemaForRead("coinmarketcap");
2603
+ const res = await queryMarketContext(
3016
2604
  `
3017
- SELECT
2605
+ SELECT DISTINCT ON (symbol)
2606
+ source,
2607
+ symbol,
2608
+ cmc_id AS "cmcId",
2609
+ interval,
3018
2610
  ts,
3019
- trades,
3020
- whale_sides,
3021
- whale_addresses,
3022
- buy_notional_usd,
3023
- sell_notional_usd,
3024
- position_aware_whale_sides,
3025
- long_entry_whale_addresses,
3026
- short_entry_whale_addresses,
3027
- long_exit_whale_addresses,
3028
- short_exit_whale_addresses,
3029
- long_entry_notional_usd,
3030
- short_entry_notional_usd,
3031
- long_exit_notional_usd,
3032
- short_exit_notional_usd
3033
- FROM hyperliquid_whale_flow
3034
- WHERE symbol = $1
3035
- AND interval = '1m'
3036
- AND universe_fingerprint = $2
3037
- AND whale_registry_fingerprint = $3
3038
- AND ts >= to_timestamp($4/1000.0)
3039
- AND ts < to_timestamp($5/1000.0)
3040
- ORDER BY ts
2611
+ open_usd AS "openUsd",
2612
+ high_usd AS "highUsd",
2613
+ low_usd AS "lowUsd",
2614
+ close_usd AS "closeUsd",
2615
+ volume_usd AS "volumeUsd",
2616
+ market_cap_usd AS "marketCapUsd"
2617
+ FROM market_reference_asset_context
2618
+ WHERE source = $1
2619
+ AND symbol = ANY($2)
2620
+ AND interval = $3
2621
+ AND ts + CASE interval
2622
+ WHEN '1d' THEN interval '1 day'
2623
+ WHEN '1h' THEN interval '1 hour'
2624
+ ELSE interval '0 seconds'
2625
+ END <= to_timestamp($4/1000.0)
2626
+ ORDER BY symbol ASC, ts DESC
3041
2627
  `,
3042
- [
3043
- params.symbol,
3044
- params.universeFingerprint,
3045
- params.whaleRegistryFingerprint,
3046
- params.fromMs,
3047
- params.toMs
3048
- ],
2628
+ [source, symbols, interval, params.atMs],
3049
2629
  params
3050
2630
  );
3051
- return result.rows.map((row) => ({
3052
- ts: new Date(row.ts),
3053
- trades: Number(row.trades) || 0,
3054
- whaleSides: Number(row.whale_sides) || 0,
3055
- whaleAddresses: Array.isArray(row.whale_addresses) ? row.whale_addresses.map(String) : [],
3056
- buyNotionalUsd: Number(row.buy_notional_usd) || 0,
3057
- sellNotionalUsd: Number(row.sell_notional_usd) || 0,
3058
- positionAwareWhaleSides: Number(row.position_aware_whale_sides) || 0,
3059
- longEntryWhaleAddresses: Array.isArray(row.long_entry_whale_addresses) ? row.long_entry_whale_addresses.map(String) : [],
3060
- shortEntryWhaleAddresses: Array.isArray(row.short_entry_whale_addresses) ? row.short_entry_whale_addresses.map(String) : [],
3061
- longExitWhaleAddresses: Array.isArray(row.long_exit_whale_addresses) ? row.long_exit_whale_addresses.map(String) : [],
3062
- shortExitWhaleAddresses: Array.isArray(row.short_exit_whale_addresses) ? row.short_exit_whale_addresses.map(String) : [],
3063
- longEntryNotionalUsd: Number(row.long_entry_notional_usd) || 0,
3064
- shortEntryNotionalUsd: Number(row.short_entry_notional_usd) || 0,
3065
- longExitNotionalUsd: Number(row.long_exit_notional_usd) || 0,
3066
- shortExitNotionalUsd: Number(row.short_exit_notional_usd) || 0
3067
- }));
2631
+ for (const row of res.rows) {
2632
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
2633
+ rows.set(row.symbol.toUpperCase(), {
2634
+ ...row,
2635
+ ageMs,
2636
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
2637
+ });
2638
+ }
2639
+ return rows;
3068
2640
  }
3069
- async function getHyperliquidWhaleFlowAggregate(params) {
3070
- await prepareMarketContextSchemaForRead("hyperliquidWhales");
3071
- const intervalMs = HYPERLIQUID_CONTEXT_INTERVAL_MS[params.interval];
3072
- const expectedBuckets = Math.ceil(intervalMs / 6e4);
2641
+ async function getLatestMarketCmcExchangeLiquidityContext(params) {
2642
+ await prepareMarketContextSchemaForRead("coinmarketcap");
2643
+ const source = params.source ?? "coinmarketcap_exchange_liquidity";
2644
+ const interval = params.interval ?? "1d";
3073
2645
  const res = await queryMarketContext(
3074
2646
  `
3075
- WITH coverage_rows AS (
3076
- SELECT *
3077
- FROM hyperliquid_whale_coverage_1m
3078
- WHERE universe_fingerprint = $2
3079
- AND whale_registry_fingerprint = $3
3080
- AND data_model_version = $6
3081
- AND ts >= to_timestamp(
3082
- ($4::double precision - $5::double precision) / 1000.0
3083
- )
3084
- AND ts < to_timestamp($4/1000.0)
3085
- ), coverage_summary AS (
3086
- SELECT
3087
- COUNT(*)::int AS coverage_buckets,
3088
- MAX(ts) AS coverage_as_of_ts,
3089
- MIN(covered_whales)::int AS covered_whales,
3090
- MAX(expected_whales)::int AS expected_whales,
3091
- MIN(coverage_pct) AS coverage_pct
3092
- FROM coverage_rows
3093
- ), window_rows AS (
3094
- SELECT *
3095
- FROM hyperliquid_whale_flow
3096
- WHERE symbol = $1
3097
- AND interval = '1m'
3098
- AND universe_fingerprint = $2
3099
- AND whale_registry_fingerprint = $3
3100
- AND ts >= to_timestamp(
3101
- ($4::double precision - $5::double precision) / 1000.0
3102
- )
3103
- AND ts < to_timestamp($4/1000.0)
3104
- ), unique_addresses AS (
3105
- SELECT COUNT(DISTINCT address)::int AS unique_whales
3106
- FROM window_rows
3107
- CROSS JOIN LATERAL UNNEST(whale_addresses) AS expanded(address)
3108
- ), directional_counts AS (
3109
- SELECT
3110
- (
3111
- SELECT COUNT(DISTINCT address)::int
3112
- FROM window_rows
3113
- CROSS JOIN LATERAL UNNEST(long_entry_whale_addresses) AS expanded(address)
3114
- ) AS long_entry_whales,
3115
- (
3116
- SELECT COUNT(DISTINCT address)::int
3117
- FROM window_rows
3118
- CROSS JOIN LATERAL UNNEST(short_entry_whale_addresses) AS expanded(address)
3119
- ) AS short_entry_whales,
3120
- (
3121
- SELECT COUNT(DISTINCT address)::int
3122
- FROM window_rows
3123
- CROSS JOIN LATERAL UNNEST(long_exit_whale_addresses) AS expanded(address)
3124
- ) AS long_exit_whales,
3125
- (
3126
- SELECT COUNT(DISTINCT address)::int
3127
- FROM window_rows
3128
- CROSS JOIN LATERAL UNNEST(short_exit_whale_addresses) AS expanded(address)
3129
- ) AS short_exit_whales
3130
- )
3131
2647
  SELECT
3132
- $1::text AS symbol,
3133
- coverage_summary.coverage_as_of_ts AS as_of_ts,
3134
- coverage_summary.coverage_buckets,
3135
- coverage_summary.covered_whales,
3136
- coverage_summary.expected_whales,
3137
- coverage_summary.coverage_pct,
3138
- COALESCE((SELECT SUM(trades) FROM window_rows), 0)::int AS trades,
3139
- COALESCE((SELECT SUM(whale_sides) FROM window_rows), 0)::int AS whale_sides,
3140
- COALESCE((SELECT unique_whales FROM unique_addresses), 0)::int AS unique_whales,
3141
- COALESCE((SELECT SUM(buy_notional_usd) FROM window_rows), 0) AS buy_notional_usd,
3142
- COALESCE((SELECT SUM(sell_notional_usd) FROM window_rows), 0) AS sell_notional_usd,
3143
- COALESCE((SELECT SUM(net_notional_usd) FROM window_rows), 0) AS net_notional_usd,
3144
- CASE
3145
- WHEN COALESCE((SELECT SUM(buy_notional_usd + sell_notional_usd) FROM window_rows), 0) > 0
3146
- THEN (SELECT SUM(buy_notional_usd) FROM window_rows) /
3147
- (SELECT SUM(buy_notional_usd + sell_notional_usd) FROM window_rows)
3148
- ELSE NULL
3149
- END AS buy_share_pct,
3150
- COALESCE((SELECT SUM(position_aware_whale_sides) FROM window_rows), 0)::int
3151
- AS position_aware_whale_sides,
3152
- CASE
3153
- WHEN COALESCE((SELECT SUM(whale_sides) FROM window_rows), 0) > 0
3154
- THEN COALESCE((SELECT SUM(position_aware_whale_sides) FROM window_rows), 0)::double precision /
3155
- (SELECT SUM(whale_sides) FROM window_rows)
3156
- ELSE 0
3157
- END AS position_aware_pct,
3158
- COALESCE((SELECT long_entry_whales FROM directional_counts), 0)::int AS long_entry_whales,
3159
- COALESCE((SELECT short_entry_whales FROM directional_counts), 0)::int AS short_entry_whales,
3160
- COALESCE((SELECT long_exit_whales FROM directional_counts), 0)::int AS long_exit_whales,
3161
- COALESCE((SELECT short_exit_whales FROM directional_counts), 0)::int AS short_exit_whales,
3162
- COALESCE((SELECT SUM(long_entry_notional_usd) FROM window_rows), 0)
3163
- AS long_entry_notional_usd,
3164
- COALESCE((SELECT SUM(short_entry_notional_usd) FROM window_rows), 0)
3165
- AS short_entry_notional_usd,
3166
- COALESCE((SELECT SUM(long_exit_notional_usd) FROM window_rows), 0)
3167
- AS long_exit_notional_usd,
3168
- COALESCE((SELECT SUM(short_exit_notional_usd) FROM window_rows), 0)
3169
- AS short_exit_notional_usd,
3170
- COALESCE((SELECT SUM(entry_net_notional_usd) FROM window_rows), 0)
3171
- AS entry_net_notional_usd,
3172
- CASE
3173
- WHEN COALESCE((SELECT SUM(long_entry_notional_usd + short_entry_notional_usd) FROM window_rows), 0) > 0
3174
- THEN (SELECT SUM(long_entry_notional_usd) FROM window_rows) /
3175
- (SELECT SUM(long_entry_notional_usd + short_entry_notional_usd) FROM window_rows)
3176
- ELSE NULL
3177
- END AS entry_long_share_pct,
3178
- (SELECT MAX(source) FROM window_rows) AS source
3179
- FROM coverage_summary
2648
+ source,
2649
+ interval,
2650
+ ts,
2651
+ exchanges_count::int AS "exchangesCount",
2652
+ total_volume_usd AS "totalVolumeUsd",
2653
+ binance_volume_usd AS "binanceVolumeUsd",
2654
+ binance_volume_share AS "binanceVolumeShare",
2655
+ top_exchange_volume_share AS "topExchangeVolumeShare",
2656
+ liquidity_regime AS "liquidityRegime"
2657
+ FROM market_cmc_exchange_liquidity_context
2658
+ WHERE source = $1
2659
+ AND interval = $2
2660
+ AND ts + CASE interval
2661
+ WHEN '1d' THEN interval '1 day'
2662
+ WHEN '1h' THEN interval '1 hour'
2663
+ ELSE interval '0 seconds'
2664
+ END <= to_timestamp($3/1000.0)
2665
+ ORDER BY ts DESC
2666
+ LIMIT 1
3180
2667
  `,
3181
- [
3182
- params.symbol,
3183
- params.universeFingerprint,
3184
- params.whaleRegistryFingerprint,
3185
- params.decisionTimeMs,
3186
- intervalMs,
3187
- import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3188
- ],
2668
+ [source, interval, params.atMs],
3189
2669
  params
3190
2670
  );
3191
2671
  const row = res.rows[0];
3192
- if (!row?.as_of_ts || Number(row.coverage_buckets) !== expectedBuckets || Number(row.covered_whales) <= 0) {
3193
- return null;
3194
- }
3195
- const asOfTs = new Date(row.as_of_ts);
3196
- const ageMs = params.decisionTimeMs - (asOfTs.getTime() + 6e4);
2672
+ if (!row) return null;
2673
+ const previousRes = await queryMarketContext(
2674
+ `
2675
+ SELECT total_volume_usd AS "totalVolumeUsd"
2676
+ FROM market_cmc_exchange_liquidity_context
2677
+ WHERE source = $1
2678
+ AND interval = $2
2679
+ AND ts <= $3::timestamptz - interval '24 hours'
2680
+ ORDER BY ts DESC
2681
+ LIMIT 1
2682
+ `,
2683
+ [source, interval, row.ts],
2684
+ params
2685
+ );
2686
+ const currentTotal = row.totalVolumeUsd == null ? null : Number(row.totalVolumeUsd);
2687
+ const previousTotal = previousRes.rows[0]?.totalVolumeUsd == null ? null : Number(previousRes.rows[0].totalVolumeUsd);
2688
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3197
2689
  return {
3198
- symbol: params.symbol,
3199
- interval: params.interval,
3200
- asOfTs,
3201
- windowEndTs: new Date(params.decisionTimeMs),
3202
- trades: Number(row.trades) || 0,
3203
- whaleSides: Number(row.whale_sides) || 0,
3204
- uniqueWhales: Number(row.unique_whales) || 0,
3205
- coveredWhales: Number(row.covered_whales) || 0,
3206
- expectedWhales: Number(row.expected_whales) || 0,
3207
- coveragePct: Number(row.coverage_pct) || 0,
3208
- buyNotionalUsd: Number(row.buy_notional_usd) || 0,
3209
- sellNotionalUsd: Number(row.sell_notional_usd) || 0,
3210
- netNotionalUsd: Number(row.net_notional_usd) || 0,
3211
- buySharePct: row.buy_share_pct == null ? null : Number(row.buy_share_pct) || 0,
3212
- positionAwareWhaleSides: Number(row.position_aware_whale_sides) || 0,
3213
- positionAwarePct: Number(row.position_aware_pct) || 0,
3214
- longEntryWhales: Number(row.long_entry_whales) || 0,
3215
- shortEntryWhales: Number(row.short_entry_whales) || 0,
3216
- longExitWhales: Number(row.long_exit_whales) || 0,
3217
- shortExitWhales: Number(row.short_exit_whales) || 0,
3218
- longEntryNotionalUsd: Number(row.long_entry_notional_usd) || 0,
3219
- shortEntryNotionalUsd: Number(row.short_entry_notional_usd) || 0,
3220
- longExitNotionalUsd: Number(row.long_exit_notional_usd) || 0,
3221
- shortExitNotionalUsd: Number(row.short_exit_notional_usd) || 0,
3222
- entryNetNotionalUsd: Number(row.entry_net_notional_usd) || 0,
3223
- entryLongSharePct: row.entry_long_share_pct == null ? null : Number(row.entry_long_share_pct) || 0,
3224
- universeFingerprint: params.universeFingerprint,
3225
- whaleRegistryFingerprint: params.whaleRegistryFingerprint,
3226
- source: row.source == null ? null : String(row.source),
2690
+ ...row,
3227
2691
  ageMs,
3228
- stale: ageMs < 0 || (params.maxAgeMs != null && Number.isFinite(params.maxAgeMs) ? ageMs > params.maxAgeMs : false)
2692
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
2693
+ totalVolumeChange24hPct: currentTotal != null && previousTotal != null && previousTotal > 0 ? (currentTotal - previousTotal) / previousTotal : null
3229
2694
  };
3230
2695
  }
3231
- async function hasHyperliquidWhaleBackfillCoverage(params) {
3232
- await ensureHyperliquidWhaleSchema();
3233
- const expectedBuckets = Math.max(
3234
- 0,
3235
- Math.ceil((params.toMs - params.fromMs) / 6e4)
3236
- );
3237
- const result = await getPool().query(
3238
- `
3239
- SELECT
3240
- COUNT(*)::int AS buckets,
3241
- COUNT(*) FILTER (
3242
- WHERE covered_whales = expected_whales
3243
- )::int AS complete_buckets
3244
- FROM hyperliquid_whale_coverage_1m
3245
- WHERE universe_fingerprint = $1
3246
- AND whale_registry_fingerprint = $2
3247
- AND data_model_version = $5
3248
- AND ts >= to_timestamp($3/1000.0)
3249
- AND ts < to_timestamp($4/1000.0)
3250
- `,
3251
- [
3252
- params.universeFingerprint,
3253
- params.whaleRegistryFingerprint,
3254
- params.fromMs,
3255
- params.toMs,
3256
- import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3257
- ]
3258
- );
3259
- return Number(result.rows[0]?.buckets) === expectedBuckets && Number(result.rows[0]?.complete_buckets) === expectedBuckets;
3260
- }
3261
- async function getLatestMarketBreadth(params) {
3262
- await prepareMarketContextSchemaForRead("binance");
2696
+ async function getLatestMarketCmcIndexContexts(params) {
2697
+ const source = params.source ?? "coinmarketcap_index";
2698
+ const interval = params.interval ?? "1d";
2699
+ const indexSlugs = [
2700
+ ...new Set(
2701
+ params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
2702
+ (slug) => ["cmc100", "cmc20"].includes(slug)
2703
+ )
2704
+ )
2705
+ ];
2706
+ const rows = /* @__PURE__ */ new Map();
2707
+ if (!indexSlugs.length) return rows;
2708
+ await prepareMarketContextSchemaForRead("coinmarketcap");
3263
2709
  const res = await queryMarketContext(
3264
2710
  `
3265
- SELECT
3266
- universe,
2711
+ SELECT DISTINCT ON (index_slug)
2712
+ source,
2713
+ index_slug AS "indexSlug",
3267
2714
  interval,
3268
2715
  ts,
3269
- symbols_count::int AS "symbolsCount",
3270
- advancers::int AS advancers,
3271
- decliners::int AS decliners,
3272
- unchanged::int AS unchanged,
3273
- advance_decline_ratio AS "advanceDeclineRatio",
3274
- pct_above_ma20 AS "pctAboveMa20",
3275
- pct_above_ma50 AS "pctAboveMa50",
3276
- equal_weighted_return AS "equalWeightedReturn",
3277
- volume_weighted_return AS "volumeWeightedReturn",
3278
- dispersion,
3279
- btc_return_1h AS "btcReturn1h",
3280
- btc_return_4h AS "btcReturn4h",
3281
- btc_return_24h AS "btcReturn24h",
3282
- alt_basket_return_1h AS "altBasketReturn1h",
3283
- alt_basket_return_4h AS "altBasketReturn4h",
3284
- alt_basket_return_24h AS "altBasketReturn24h",
3285
- btc_vs_alt_return_1h AS "btcVsAltReturn1h",
3286
- btc_vs_alt_return_4h AS "btcVsAltReturn4h",
3287
- btc_vs_alt_return_24h AS "btcVsAltReturn24h",
3288
- btc_turnover_share_1h AS "btcTurnoverShare1h",
3289
- btc_turnover_share_24h AS "btcTurnoverShare24h",
3290
- btc_turnover_share_change_24h AS "btcTurnoverShareChange24h",
3291
- alt_vol_to_btc_vol_24h AS "altVolToBtcVol24h",
3292
- alt_dispersion_24h AS "altDispersion24h",
3293
- btc_alt_regime AS "btcAltRegime",
3294
- source
3295
- FROM market_breadth
3296
- WHERE universe = $1
3297
- AND interval = $2
3298
- AND ts <= to_timestamp($3/1000.0)
3299
- ORDER BY ts DESC
3300
- LIMIT 1
2716
+ value,
2717
+ constituents_count::int AS "constituentsCount",
2718
+ top_constituent_symbol AS "topConstituentSymbol",
2719
+ top_constituent_weight_pct AS "topConstituentWeightPct",
2720
+ constituents
2721
+ FROM market_cmc_index_context
2722
+ WHERE source = $1
2723
+ AND index_slug = ANY($2)
2724
+ AND interval = $3
2725
+ AND ts + CASE interval
2726
+ WHEN '1d' THEN interval '1 day'
2727
+ WHEN '1h' THEN interval '1 hour'
2728
+ ELSE interval '0 seconds'
2729
+ END <= to_timestamp($4/1000.0)
2730
+ ORDER BY index_slug ASC, ts DESC
3301
2731
  `,
3302
- [params.universe, params.interval, params.atMs],
2732
+ [source, indexSlugs, interval, params.atMs],
3303
2733
  params
3304
2734
  );
3305
- const row = res.rows[0];
3306
- if (!row) return null;
3307
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3308
- return {
3309
- ...row,
3310
- ageMs,
3311
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
3312
- };
2735
+ for (const row of res.rows) {
2736
+ const previousRes = await queryMarketContext(
2737
+ `
2738
+ SELECT value
2739
+ FROM market_cmc_index_context
2740
+ WHERE source = $1
2741
+ AND index_slug = $2
2742
+ AND interval = $3
2743
+ AND ts <= $4::timestamptz - interval '24 hours'
2744
+ ORDER BY ts DESC
2745
+ LIMIT 1
2746
+ `,
2747
+ [source, row.indexSlug, interval, row.ts],
2748
+ params
2749
+ );
2750
+ const currentValue = row.value == null ? null : Number(row.value);
2751
+ const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
2752
+ const ageMs = toMarketFeatureAge(row.ts, params.atMs);
2753
+ rows.set(row.indexSlug, {
2754
+ ...row,
2755
+ ageMs,
2756
+ stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
2757
+ valueChange24hPct: currentValue != null && previousValue != null && previousValue > 0 ? (currentValue - previousValue) / previousValue : null
2758
+ });
2759
+ }
2760
+ return rows;
3313
2761
  }
3314
- async function getLatestMarketGlobalContext(params) {
2762
+ async function getLatestMarketCmcFearGreedContext(params) {
3315
2763
  await prepareMarketContextSchemaForRead("coinmarketcap");
3316
- const source = params.source ?? "coinmarketcap_global";
2764
+ const source = params.source ?? "coinmarketcap_fear_greed";
2765
+ const interval = params.interval ?? "1d";
3317
2766
  const res = await queryMarketContext(
3318
2767
  `
3319
2768
  SELECT
3320
2769
  source,
2770
+ interval,
3321
2771
  ts,
3322
- updated_at_ts AS "updatedAt",
3323
- active_cryptocurrencies::int AS "activeCryptocurrencies",
3324
- active_exchanges::int AS "activeExchanges",
3325
- active_market_pairs::int AS "activeMarketPairs",
3326
- markets::int AS markets,
3327
- total_market_cap_usd AS "totalMarketCapUsd",
3328
- total_volume_usd AS "totalVolumeUsd",
3329
- total_volume_reported_usd AS "totalVolumeReportedUsd",
3330
- btc_dominance_pct AS "btcDominancePct",
3331
- eth_dominance_pct AS "ethDominancePct",
3332
- alt_market_cap_usd AS "altMarketCapUsd",
3333
- alt_volume_usd AS "altVolumeUsd",
3334
- alt_volume_reported_usd AS "altVolumeReportedUsd",
3335
- btc_to_alt_market_cap_ratio AS "btcToAltMarketCapRatio",
3336
- market_cap_change_pct_24h_usd AS "marketCapChangePct24hUsd"
3337
- FROM market_global_context
2772
+ value::int AS value,
2773
+ classification,
2774
+ sentiment_regime AS "sentimentRegime"
2775
+ FROM market_cmc_fear_greed_context
3338
2776
  WHERE source = $1
3339
- AND ts + CASE
3340
- WHEN source = 'coinmarketcap_global' THEN interval '1 day'
2777
+ AND interval = $2
2778
+ AND ts + CASE interval
2779
+ WHEN '1d' THEN interval '1 day'
2780
+ WHEN '1h' THEN interval '1 hour'
3341
2781
  ELSE interval '0 seconds'
3342
- END <= to_timestamp($2/1000.0)
2782
+ END <= to_timestamp($3/1000.0)
3343
2783
  ORDER BY ts DESC
3344
2784
  LIMIT 1
3345
2785
  `,
3346
- [source, params.atMs],
2786
+ [source, interval, params.atMs],
3347
2787
  params
3348
2788
  );
3349
2789
  const row = res.rows[0];
@@ -3351,39 +2791,43 @@ async function getLatestMarketGlobalContext(params) {
3351
2791
  const previousRes = await queryMarketContext(
3352
2792
  `
3353
2793
  SELECT
3354
- btc_dominance_pct AS "btcDominancePct",
3355
- eth_dominance_pct AS "ethDominancePct",
3356
- alt_market_cap_usd AS "altMarketCapUsd",
3357
- alt_volume_usd AS "altVolumeUsd"
3358
- FROM market_global_context
2794
+ value::int AS value,
2795
+ '24h' AS bucket
2796
+ FROM market_cmc_fear_greed_context
3359
2797
  WHERE source = $1
3360
- AND ts <= $2::timestamptz - interval '24 hours'
2798
+ AND interval = $2
2799
+ AND ts <= $3::timestamptz - interval '24 hours'
3361
2800
  ORDER BY ts DESC
3362
2801
  LIMIT 1
3363
2802
  `,
3364
- [source, row.ts],
2803
+ [source, interval, row.ts],
3365
2804
  params
3366
2805
  );
3367
- const previousDominance = previousRes.rows[0]?.btcDominancePct == null ? null : Number(previousRes.rows[0].btcDominancePct);
3368
- const previousEthDominance = previousRes.rows[0]?.ethDominancePct == null ? null : Number(previousRes.rows[0].ethDominancePct);
3369
- const previousAltMarketCap = previousRes.rows[0]?.altMarketCapUsd == null ? null : Number(previousRes.rows[0].altMarketCapUsd);
3370
- const previousAltVolume = previousRes.rows[0]?.altVolumeUsd == null ? null : Number(previousRes.rows[0].altVolumeUsd);
3371
- const currentDominance = row.btcDominancePct == null ? null : Number(row.btcDominancePct);
3372
- const currentEthDominance = row.ethDominancePct == null ? null : Number(row.ethDominancePct);
3373
- const currentAltMarketCap = row.altMarketCapUsd == null ? null : Number(row.altMarketCapUsd);
3374
- const currentAltVolume = row.altVolumeUsd == null ? null : Number(row.altVolumeUsd);
2806
+ const previous7dRes = await queryMarketContext(
2807
+ `
2808
+ SELECT value::int AS value
2809
+ FROM market_cmc_fear_greed_context
2810
+ WHERE source = $1
2811
+ AND interval = $2
2812
+ AND ts <= $3::timestamptz - interval '7 days'
2813
+ ORDER BY ts DESC
2814
+ LIMIT 1
2815
+ `,
2816
+ [source, interval, row.ts],
2817
+ params
2818
+ );
2819
+ const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
2820
+ const previous7dValue = previous7dRes.rows[0]?.value == null ? null : Number(previous7dRes.rows[0].value);
3375
2821
  const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3376
2822
  return {
3377
2823
  ...row,
3378
2824
  ageMs,
3379
2825
  stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3380
- btcDominanceChange24hPct: currentDominance != null && previousDominance != null ? currentDominance - previousDominance : null,
3381
- ethDominanceChange24hPct: currentEthDominance != null && previousEthDominance != null ? currentEthDominance - previousEthDominance : null,
3382
- altMarketCapChange24hPct: currentAltMarketCap != null && previousAltMarketCap != null && previousAltMarketCap > 0 ? (currentAltMarketCap - previousAltMarketCap) / previousAltMarketCap : null,
3383
- altVolumeChange24hPct: currentAltVolume != null && previousAltVolume != null && previousAltVolume > 0 ? (currentAltVolume - previousAltVolume) / previousAltVolume : null
2826
+ valueChange24h: previousValue == null ? null : row.value - previousValue,
2827
+ valueChange7d: previous7dValue == null ? null : row.value - previous7dValue
3384
2828
  };
3385
2829
  }
3386
- async function getMarketGlobalContextCoverage(params) {
2830
+ async function getMarketCmcFearGreedContextCoverage(params) {
3387
2831
  await ensureBinanceMarketSchema();
3388
2832
  const pool = getPool();
3389
2833
  const res = await pool.query(
@@ -3392,166 +2836,48 @@ async function getMarketGlobalContextCoverage(params) {
3392
2836
  extract(epoch from MIN(ts))*1000 AS first_ms,
3393
2837
  extract(epoch from MAX(ts))*1000 AS last_ms,
3394
2838
  COUNT(*)::int AS rows
3395
- FROM market_global_context
2839
+ FROM market_cmc_fear_greed_context
3396
2840
  WHERE source = $1
3397
- AND ts >= to_timestamp($2/1000.0)
3398
- AND ts <= to_timestamp($3/1000.0)
2841
+ AND interval = $2
2842
+ AND ts >= to_timestamp($3/1000.0)
2843
+ AND ts <= to_timestamp($4/1000.0)
3399
2844
  `,
3400
- [params.source, params.startMs, params.endMs]
2845
+ [params.source, params.interval, params.startMs, params.endMs]
3401
2846
  );
3402
- const row = res.rows[0];
3403
- const rows = Number(row?.rows ?? 0);
3404
- const firstMs = Number(row?.first_ms);
3405
- const lastMs = Number(row?.last_ms);
2847
+ const rows = Number(res.rows[0]?.rows ?? 0);
2848
+ const firstMs = Number(res.rows[0]?.first_ms);
2849
+ const lastMs = Number(res.rows[0]?.last_ms);
3406
2850
  if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
3407
2851
  return null;
3408
2852
  }
3409
2853
  return { firstMs, lastMs, rows };
3410
2854
  }
3411
- async function getMarketReferenceAssetContextCoverage(params) {
3412
- const symbols = [
3413
- ...new Set(
3414
- params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
3415
- )
3416
- ];
3417
- const coverage = /* @__PURE__ */ new Map();
3418
- if (!symbols.length) return coverage;
2855
+ async function getMarketCmcExchangeLiquidityContextCoverage(params) {
3419
2856
  await ensureBinanceMarketSchema();
3420
2857
  const pool = getPool();
3421
2858
  const res = await pool.query(
3422
2859
  `
3423
2860
  SELECT
3424
- symbol,
3425
2861
  extract(epoch from MIN(ts))*1000 AS first_ms,
3426
2862
  extract(epoch from MAX(ts))*1000 AS last_ms,
3427
2863
  COUNT(*)::int AS rows
3428
- FROM market_reference_asset_context
2864
+ FROM market_cmc_exchange_liquidity_context
3429
2865
  WHERE source = $1
3430
- AND symbol = ANY($2)
3431
- AND interval = $3
3432
- AND ts >= to_timestamp($4/1000.0)
3433
- AND ts <= to_timestamp($5/1000.0)
3434
- GROUP BY symbol
2866
+ AND interval = $2
2867
+ AND ts >= to_timestamp($3/1000.0)
2868
+ AND ts <= to_timestamp($4/1000.0)
3435
2869
  `,
3436
- [params.source, symbols, params.interval, params.startMs, params.endMs]
2870
+ [params.source, params.interval, params.startMs, params.endMs]
3437
2871
  );
3438
- for (const row of res.rows) {
3439
- const firstMs = Number(row.first_ms);
3440
- const lastMs = Number(row.last_ms);
3441
- const rows = Number(row.rows);
3442
- if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
3443
- coverage.set(row.symbol.toUpperCase(), { firstMs, lastMs, rows });
3444
- }
2872
+ const rows = Number(res.rows[0]?.rows ?? 0);
2873
+ const firstMs = Number(res.rows[0]?.first_ms);
2874
+ const lastMs = Number(res.rows[0]?.last_ms);
2875
+ if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
2876
+ return null;
3445
2877
  }
3446
- return coverage;
2878
+ return { firstMs, lastMs, rows };
3447
2879
  }
3448
- async function getLatestMarketReferenceAssetContexts(params) {
3449
- const source = params.source ?? "coinmarketcap_reference_asset";
3450
- const interval = params.interval ?? "1d";
3451
- const symbols = [
3452
- ...new Set(
3453
- params.symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean)
3454
- )
3455
- ];
3456
- const rows = /* @__PURE__ */ new Map();
3457
- if (!symbols.length) return rows;
3458
- await prepareMarketContextSchemaForRead("coinmarketcap");
3459
- const res = await queryMarketContext(
3460
- `
3461
- SELECT DISTINCT ON (symbol)
3462
- source,
3463
- symbol,
3464
- cmc_id AS "cmcId",
3465
- interval,
3466
- ts,
3467
- open_usd AS "openUsd",
3468
- high_usd AS "highUsd",
3469
- low_usd AS "lowUsd",
3470
- close_usd AS "closeUsd",
3471
- volume_usd AS "volumeUsd",
3472
- market_cap_usd AS "marketCapUsd"
3473
- FROM market_reference_asset_context
3474
- WHERE source = $1
3475
- AND symbol = ANY($2)
3476
- AND interval = $3
3477
- AND ts + CASE interval
3478
- WHEN '1d' THEN interval '1 day'
3479
- WHEN '1h' THEN interval '1 hour'
3480
- ELSE interval '0 seconds'
3481
- END <= to_timestamp($4/1000.0)
3482
- ORDER BY symbol ASC, ts DESC
3483
- `,
3484
- [source, symbols, interval, params.atMs],
3485
- params
3486
- );
3487
- for (const row of res.rows) {
3488
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3489
- rows.set(row.symbol.toUpperCase(), {
3490
- ...row,
3491
- ageMs,
3492
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs
3493
- });
3494
- }
3495
- return rows;
3496
- }
3497
- async function getLatestMarketCmcExchangeLiquidityContext(params) {
3498
- await prepareMarketContextSchemaForRead("coinmarketcap");
3499
- const source = params.source ?? "coinmarketcap_exchange_liquidity";
3500
- const interval = params.interval ?? "1d";
3501
- const res = await queryMarketContext(
3502
- `
3503
- SELECT
3504
- source,
3505
- interval,
3506
- ts,
3507
- exchanges_count::int AS "exchangesCount",
3508
- total_volume_usd AS "totalVolumeUsd",
3509
- binance_volume_usd AS "binanceVolumeUsd",
3510
- binance_volume_share AS "binanceVolumeShare",
3511
- top_exchange_volume_share AS "topExchangeVolumeShare",
3512
- liquidity_regime AS "liquidityRegime"
3513
- FROM market_cmc_exchange_liquidity_context
3514
- WHERE source = $1
3515
- AND interval = $2
3516
- AND ts + CASE interval
3517
- WHEN '1d' THEN interval '1 day'
3518
- WHEN '1h' THEN interval '1 hour'
3519
- ELSE interval '0 seconds'
3520
- END <= to_timestamp($3/1000.0)
3521
- ORDER BY ts DESC
3522
- LIMIT 1
3523
- `,
3524
- [source, interval, params.atMs],
3525
- params
3526
- );
3527
- const row = res.rows[0];
3528
- if (!row) return null;
3529
- const previousRes = await queryMarketContext(
3530
- `
3531
- SELECT total_volume_usd AS "totalVolumeUsd"
3532
- FROM market_cmc_exchange_liquidity_context
3533
- WHERE source = $1
3534
- AND interval = $2
3535
- AND ts <= $3::timestamptz - interval '24 hours'
3536
- ORDER BY ts DESC
3537
- LIMIT 1
3538
- `,
3539
- [source, interval, row.ts],
3540
- params
3541
- );
3542
- const currentTotal = row.totalVolumeUsd == null ? null : Number(row.totalVolumeUsd);
3543
- const previousTotal = previousRes.rows[0]?.totalVolumeUsd == null ? null : Number(previousRes.rows[0].totalVolumeUsd);
3544
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3545
- return {
3546
- ...row,
3547
- ageMs,
3548
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3549
- totalVolumeChange24hPct: currentTotal != null && previousTotal != null && previousTotal > 0 ? (currentTotal - previousTotal) / previousTotal : null
3550
- };
3551
- }
3552
- async function getLatestMarketCmcIndexContexts(params) {
3553
- const source = params.source ?? "coinmarketcap_index";
3554
- const interval = params.interval ?? "1d";
2880
+ async function getMarketCmcIndexContextCoverage(params) {
3555
2881
  const indexSlugs = [
3556
2882
  ...new Set(
3557
2883
  params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
@@ -3559,233 +2885,51 @@ async function getLatestMarketCmcIndexContexts(params) {
3559
2885
  )
3560
2886
  )
3561
2887
  ];
3562
- const rows = /* @__PURE__ */ new Map();
3563
- if (!indexSlugs.length) return rows;
3564
- await prepareMarketContextSchemaForRead("coinmarketcap");
3565
- const res = await queryMarketContext(
2888
+ const coverage = /* @__PURE__ */ new Map();
2889
+ if (!indexSlugs.length) return coverage;
2890
+ await ensureBinanceMarketSchema();
2891
+ const pool = getPool();
2892
+ const res = await pool.query(
3566
2893
  `
3567
- SELECT DISTINCT ON (index_slug)
3568
- source,
3569
- index_slug AS "indexSlug",
3570
- interval,
3571
- ts,
3572
- value,
3573
- constituents_count::int AS "constituentsCount",
3574
- top_constituent_symbol AS "topConstituentSymbol",
3575
- top_constituent_weight_pct AS "topConstituentWeightPct",
3576
- constituents
2894
+ SELECT
2895
+ index_slug,
2896
+ extract(epoch from MIN(ts))*1000 AS first_ms,
2897
+ extract(epoch from MAX(ts))*1000 AS last_ms,
2898
+ COUNT(*)::int AS rows
3577
2899
  FROM market_cmc_index_context
3578
2900
  WHERE source = $1
3579
2901
  AND index_slug = ANY($2)
3580
2902
  AND interval = $3
3581
- AND ts + CASE interval
3582
- WHEN '1d' THEN interval '1 day'
3583
- WHEN '1h' THEN interval '1 hour'
3584
- ELSE interval '0 seconds'
3585
- END <= to_timestamp($4/1000.0)
3586
- ORDER BY index_slug ASC, ts DESC
2903
+ AND ts >= to_timestamp($4/1000.0)
2904
+ AND ts <= to_timestamp($5/1000.0)
2905
+ GROUP BY index_slug
3587
2906
  `,
3588
- [source, indexSlugs, interval, params.atMs],
3589
- params
2907
+ [params.source, indexSlugs, params.interval, params.startMs, params.endMs]
3590
2908
  );
3591
2909
  for (const row of res.rows) {
3592
- const previousRes = await queryMarketContext(
3593
- `
3594
- SELECT value
3595
- FROM market_cmc_index_context
3596
- WHERE source = $1
3597
- AND index_slug = $2
3598
- AND interval = $3
3599
- AND ts <= $4::timestamptz - interval '24 hours'
3600
- ORDER BY ts DESC
3601
- LIMIT 1
3602
- `,
3603
- [source, row.indexSlug, interval, row.ts],
3604
- params
3605
- );
3606
- const currentValue = row.value == null ? null : Number(row.value);
3607
- const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
3608
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3609
- rows.set(row.indexSlug, {
3610
- ...row,
3611
- ageMs,
3612
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3613
- valueChange24hPct: currentValue != null && previousValue != null && previousValue > 0 ? (currentValue - previousValue) / previousValue : null
3614
- });
2910
+ const indexSlug = row.index_slug;
2911
+ const firstMs = Number(row.first_ms);
2912
+ const lastMs = Number(row.last_ms);
2913
+ const rows = Number(row.rows);
2914
+ if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
2915
+ coverage.set(indexSlug, { firstMs, lastMs, rows });
2916
+ }
3615
2917
  }
3616
- return rows;
3617
- }
3618
- async function getLatestMarketCmcFearGreedContext(params) {
3619
- await prepareMarketContextSchemaForRead("coinmarketcap");
3620
- const source = params.source ?? "coinmarketcap_fear_greed";
3621
- const interval = params.interval ?? "1d";
3622
- const res = await queryMarketContext(
3623
- `
3624
- SELECT
3625
- source,
3626
- interval,
3627
- ts,
3628
- value::int AS value,
3629
- classification,
3630
- sentiment_regime AS "sentimentRegime"
3631
- FROM market_cmc_fear_greed_context
3632
- WHERE source = $1
3633
- AND interval = $2
3634
- AND ts + CASE interval
3635
- WHEN '1d' THEN interval '1 day'
3636
- WHEN '1h' THEN interval '1 hour'
3637
- ELSE interval '0 seconds'
3638
- END <= to_timestamp($3/1000.0)
3639
- ORDER BY ts DESC
3640
- LIMIT 1
3641
- `,
3642
- [source, interval, params.atMs],
3643
- params
3644
- );
3645
- const row = res.rows[0];
3646
- if (!row) return null;
3647
- const previousRes = await queryMarketContext(
3648
- `
3649
- SELECT
3650
- value::int AS value,
3651
- '24h' AS bucket
3652
- FROM market_cmc_fear_greed_context
3653
- WHERE source = $1
3654
- AND interval = $2
3655
- AND ts <= $3::timestamptz - interval '24 hours'
3656
- ORDER BY ts DESC
3657
- LIMIT 1
3658
- `,
3659
- [source, interval, row.ts],
3660
- params
3661
- );
3662
- const previous7dRes = await queryMarketContext(
3663
- `
3664
- SELECT value::int AS value
3665
- FROM market_cmc_fear_greed_context
3666
- WHERE source = $1
3667
- AND interval = $2
3668
- AND ts <= $3::timestamptz - interval '7 days'
3669
- ORDER BY ts DESC
3670
- LIMIT 1
3671
- `,
3672
- [source, interval, row.ts],
3673
- params
3674
- );
3675
- const previousValue = previousRes.rows[0]?.value == null ? null : Number(previousRes.rows[0].value);
3676
- const previous7dValue = previous7dRes.rows[0]?.value == null ? null : Number(previous7dRes.rows[0].value);
3677
- const ageMs = toMarketFeatureAge(row.ts, params.atMs);
3678
- return {
3679
- ...row,
3680
- ageMs,
3681
- stale: ageMs == null || params.maxAgeMs != null && ageMs > params.maxAgeMs,
3682
- valueChange24h: previousValue == null ? null : row.value - previousValue,
3683
- valueChange7d: previous7dValue == null ? null : row.value - previous7dValue
3684
- };
2918
+ return coverage;
3685
2919
  }
3686
- async function getMarketCmcFearGreedContextCoverage(params) {
2920
+ async function getMarketTradeFlowCoverage(params) {
2921
+ const symbols = [
2922
+ ...new Set(params.symbols.map((item) => item.toUpperCase()))
2923
+ ];
2924
+ if (!symbols.length) return /* @__PURE__ */ new Map();
3687
2925
  await ensureBinanceMarketSchema();
3688
2926
  const pool = getPool();
3689
2927
  const res = await pool.query(
3690
2928
  `
3691
2929
  SELECT
3692
- extract(epoch from MIN(ts))*1000 AS first_ms,
3693
- extract(epoch from MAX(ts))*1000 AS last_ms,
3694
- COUNT(*)::int AS rows
3695
- FROM market_cmc_fear_greed_context
3696
- WHERE source = $1
3697
- AND interval = $2
3698
- AND ts >= to_timestamp($3/1000.0)
3699
- AND ts <= to_timestamp($4/1000.0)
3700
- `,
3701
- [params.source, params.interval, params.startMs, params.endMs]
3702
- );
3703
- const rows = Number(res.rows[0]?.rows ?? 0);
3704
- const firstMs = Number(res.rows[0]?.first_ms);
3705
- const lastMs = Number(res.rows[0]?.last_ms);
3706
- if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
3707
- return null;
3708
- }
3709
- return { firstMs, lastMs, rows };
3710
- }
3711
- async function getMarketCmcExchangeLiquidityContextCoverage(params) {
3712
- await ensureBinanceMarketSchema();
3713
- const pool = getPool();
3714
- const res = await pool.query(
3715
- `
3716
- SELECT
3717
- extract(epoch from MIN(ts))*1000 AS first_ms,
3718
- extract(epoch from MAX(ts))*1000 AS last_ms,
3719
- COUNT(*)::int AS rows
3720
- FROM market_cmc_exchange_liquidity_context
3721
- WHERE source = $1
3722
- AND interval = $2
3723
- AND ts >= to_timestamp($3/1000.0)
3724
- AND ts <= to_timestamp($4/1000.0)
3725
- `,
3726
- [params.source, params.interval, params.startMs, params.endMs]
3727
- );
3728
- const rows = Number(res.rows[0]?.rows ?? 0);
3729
- const firstMs = Number(res.rows[0]?.first_ms);
3730
- const lastMs = Number(res.rows[0]?.last_ms);
3731
- if (!rows || !Number.isFinite(firstMs) || !Number.isFinite(lastMs)) {
3732
- return null;
3733
- }
3734
- return { firstMs, lastMs, rows };
3735
- }
3736
- async function getMarketCmcIndexContextCoverage(params) {
3737
- const indexSlugs = [
3738
- ...new Set(
3739
- params.indexSlugs.map((slug) => slug.trim().toLowerCase()).filter(
3740
- (slug) => ["cmc100", "cmc20"].includes(slug)
3741
- )
3742
- )
3743
- ];
3744
- const coverage = /* @__PURE__ */ new Map();
3745
- if (!indexSlugs.length) return coverage;
3746
- await ensureBinanceMarketSchema();
3747
- const pool = getPool();
3748
- const res = await pool.query(
3749
- `
3750
- SELECT
3751
- index_slug,
3752
- extract(epoch from MIN(ts))*1000 AS first_ms,
3753
- extract(epoch from MAX(ts))*1000 AS last_ms,
3754
- COUNT(*)::int AS rows
3755
- FROM market_cmc_index_context
3756
- WHERE source = $1
3757
- AND index_slug = ANY($2)
3758
- AND interval = $3
3759
- AND ts >= to_timestamp($4/1000.0)
3760
- AND ts <= to_timestamp($5/1000.0)
3761
- GROUP BY index_slug
3762
- `,
3763
- [params.source, indexSlugs, params.interval, params.startMs, params.endMs]
3764
- );
3765
- for (const row of res.rows) {
3766
- const indexSlug = row.index_slug;
3767
- const firstMs = Number(row.first_ms);
3768
- const lastMs = Number(row.last_ms);
3769
- const rows = Number(row.rows);
3770
- if (Number.isFinite(firstMs) && Number.isFinite(lastMs) && rows > 0) {
3771
- coverage.set(indexSlug, { firstMs, lastMs, rows });
3772
- }
3773
- }
3774
- return coverage;
3775
- }
3776
- async function getMarketTradeFlowCoverage(params) {
3777
- const symbols = [
3778
- ...new Set(params.symbols.map((item) => item.toUpperCase()))
3779
- ];
3780
- if (!symbols.length) return /* @__PURE__ */ new Map();
3781
- await ensureBinanceMarketSchema();
3782
- const pool = getPool();
3783
- const res = await pool.query(
3784
- `
3785
- SELECT
3786
- symbol,
3787
- MIN(ts) AS first_ts,
3788
- MAX(ts) AS last_ts,
2930
+ symbol,
2931
+ MIN(ts) AS first_ts,
2932
+ MAX(ts) AS last_ts,
3789
2933
  COUNT(*)::int AS rows
3790
2934
  FROM market_trade_flow
3791
2935
  WHERE symbol = ANY($1)
@@ -3926,232 +3070,1097 @@ async function getMarketBreadthCoverage(params) {
3926
3070
  btcAltMetricsRows: Number(row.btc_alt_metrics_rows) || 0
3927
3071
  };
3928
3072
  }
3929
- async function getSpreadRangeForSymbols(symbols, interval, startMs, endMs) {
3930
- if (!symbols.length) {
3931
- return [];
3073
+
3074
+ // src/timescale/hyperliquidWhales.ts
3075
+ var import_types = require("@tradejs/types");
3076
+ var HYPERLIQUID_CONTEXT_INTERVAL_MS = {
3077
+ "1m": 6e4,
3078
+ "5m": 5 * 6e4,
3079
+ "15m": 15 * 6e4,
3080
+ "1h": 60 * 6e4
3081
+ };
3082
+ async function upsertHyperliquidWhaleTradeEvents(rows) {
3083
+ if (!rows.length) return;
3084
+ await ensureHyperliquidWhaleSchema();
3085
+ const cols = [
3086
+ "symbol",
3087
+ "ts",
3088
+ "tid",
3089
+ "price",
3090
+ "size",
3091
+ "notional_usd",
3092
+ "buyer_address",
3093
+ "seller_address",
3094
+ "buyer_tracked",
3095
+ "seller_tracked",
3096
+ "buyer_start_position",
3097
+ "buyer_end_position",
3098
+ "buyer_position_action",
3099
+ "buyer_closed_pnl",
3100
+ "buyer_liquidation",
3101
+ "seller_start_position",
3102
+ "seller_end_position",
3103
+ "seller_position_action",
3104
+ "seller_closed_pnl",
3105
+ "seller_liquidation",
3106
+ "universe_fingerprint",
3107
+ "whale_registry_fingerprint",
3108
+ "source"
3109
+ ];
3110
+ const maxRows = getSafeBulkInsertRows(cols.length);
3111
+ if (rows.length > maxRows) {
3112
+ for (let index = 0; index < rows.length; index += maxRows) {
3113
+ await upsertHyperliquidWhaleTradeEvents(
3114
+ rows.slice(index, index + maxRows)
3115
+ );
3116
+ }
3117
+ return;
3932
3118
  }
3933
- await ensureSpreadSchema();
3934
- const pool = getPool();
3935
- const sql = `
3936
- SELECT symbol, interval, ts, binance_price, coinbase_price, spread
3937
- FROM market_spread
3938
- WHERE symbol = ANY($1)
3939
- AND interval = $2
3940
- AND ts >= to_timestamp($3/1000.0)
3941
- AND ts <= to_timestamp($4/1000.0)
3942
- ORDER BY symbol ASC, ts ASC
3943
- `;
3944
- const res = await pool.query(sql, [symbols, interval, startMs, endMs]);
3945
- return res.rows;
3946
- }
3947
- async function getSpreadSummary(hours = 24, limit = 500) {
3948
- await ensureSpreadSchema();
3949
- const pool = getPool();
3950
- const cappedHours = Math.max(1, Math.min(24 * 30, hours));
3951
- const cappedLimit = Math.max(50, Math.min(5e3, limit));
3952
- const rowsQ = await pool.query(
3953
- `
3954
- SELECT symbol, interval, ts, binance_price, coinbase_price, spread
3955
- FROM market_spread
3956
- WHERE ts >= now() - ($1 || ' hours')::interval
3957
- ORDER BY ts DESC
3958
- LIMIT $2
3959
- `,
3960
- [String(cappedHours), cappedLimit]
3961
- );
3962
- const aggQ = await pool.query(
3119
+ const valuesSql = rows.map(
3120
+ (_, rowIndex) => `(${cols.map((__, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(",")})`
3121
+ ).join(",");
3122
+ const flat = rows.flatMap((row) => [
3123
+ row.symbol,
3124
+ row.ts,
3125
+ row.tid,
3126
+ row.price,
3127
+ row.size,
3128
+ row.notionalUsd,
3129
+ row.buyerAddress ?? null,
3130
+ row.sellerAddress ?? null,
3131
+ row.buyerTracked,
3132
+ row.sellerTracked,
3133
+ row.buyerStartPosition ?? null,
3134
+ row.buyerEndPosition ?? null,
3135
+ row.buyerPositionAction ?? null,
3136
+ row.buyerClosedPnl ?? null,
3137
+ row.buyerLiquidation ?? null,
3138
+ row.sellerStartPosition ?? null,
3139
+ row.sellerEndPosition ?? null,
3140
+ row.sellerPositionAction ?? null,
3141
+ row.sellerClosedPnl ?? null,
3142
+ row.sellerLiquidation ?? null,
3143
+ row.universeFingerprint,
3144
+ row.whaleRegistryFingerprint,
3145
+ row.source ?? null
3146
+ ]);
3147
+ await getPool().query(
3963
3148
  `
3964
- SELECT
3149
+ INSERT INTO hyperliquid_whale_trade_events (${cols.join(",")})
3150
+ VALUES ${valuesSql}
3151
+ ON CONFLICT (
3152
+ universe_fingerprint,
3153
+ whale_registry_fingerprint,
3965
3154
  symbol,
3966
- interval,
3967
- COUNT(*)::int AS points,
3968
- MAX(ts) AS last_ts,
3969
- AVG(spread) AS avg_spread,
3970
- STDDEV_POP(spread) AS std_spread
3971
- FROM market_spread
3972
- WHERE ts >= now() - ($1 || ' hours')::interval
3973
- GROUP BY symbol, interval
3974
- ORDER BY points DESC, symbol ASC
3975
- LIMIT 500
3976
- `,
3977
- [String(cappedHours)]
3978
- );
3979
- return {
3980
- rows: rowsQ.rows,
3981
- aggregates: aggQ.rows,
3982
- hours: cappedHours
3983
- };
3984
- }
3985
- async function getCandlesRange(provider, symbol, interval, startMs, endMs) {
3986
- await ensureCandlesSchema();
3987
- const pool = getPool();
3988
- const normalizedProvider = normalizeCandleProvider(provider);
3989
- const normalizedSymbol = normalizeCandleSymbol(symbol);
3990
- const sql = `
3991
- SELECT symbol, interval, ts,
3992
- open, high, low, close, volume, turnover,
3993
- taker_buy_base_volume AS "takerBuyBaseVolume",
3994
- taker_buy_quote_volume AS "takerBuyQuoteVolume",
3995
- taker_sell_base_volume AS "takerSellBaseVolume",
3996
- taker_sell_quote_volume AS "takerSellQuoteVolume"
3997
- FROM candles
3998
- WHERE provider = $1 AND symbol = $2 AND interval = $3
3999
- AND ts >= to_timestamp($4/1000.0)
4000
- AND ts <= to_timestamp($5/1000.0)
4001
- ORDER BY ts ASC
4002
- `;
4003
- const res = await pool.query(sql, [
4004
- normalizedProvider,
4005
- normalizedSymbol,
4006
- interval,
4007
- startMs,
4008
- endMs
4009
- ]);
4010
- return res.rows;
4011
- }
4012
- async function getDataEdges(provider, symbol, interval) {
4013
- await ensureCandlesSchema();
4014
- const pool = getPool();
4015
- const normalizedProvider = normalizeCandleProvider(provider);
4016
- const normalizedSymbol = normalizeCandleSymbol(symbol);
4017
- const sqlMin = `
4018
- SELECT extract(epoch from ts)*1000 AS ms
4019
- FROM candles
4020
- WHERE provider=$1 AND symbol=$2 AND interval=$3
4021
- ORDER BY ts ASC
4022
- LIMIT 1
4023
- `;
4024
- const sqlMax = `
4025
- SELECT extract(epoch from ts)*1000 AS ms
4026
- FROM candles
4027
- WHERE provider=$1 AND symbol=$2 AND interval=$3
4028
- ORDER BY ts DESC
4029
- LIMIT 1
4030
- `;
4031
- const [minQ, maxQ] = await Promise.all([
4032
- pool.query(sqlMin, [normalizedProvider, normalizedSymbol, interval]),
4033
- pool.query(sqlMax, [normalizedProvider, normalizedSymbol, interval])
4034
- ]);
4035
- const minRaw = minQ.rows[0]?.ms;
4036
- const maxRaw = maxQ.rows[0]?.ms;
4037
- const min = Number.isFinite(Number(minRaw)) ? Number(minRaw) : void 0;
4038
- const max = Number.isFinite(Number(maxRaw)) ? Number(maxRaw) : void 0;
4039
- return { min, max };
3155
+ ts,
3156
+ tid
3157
+ ) DO UPDATE SET
3158
+ buyer_address = COALESCE(
3159
+ hyperliquid_whale_trade_events.buyer_address,
3160
+ EXCLUDED.buyer_address
3161
+ ),
3162
+ seller_address = COALESCE(
3163
+ hyperliquid_whale_trade_events.seller_address,
3164
+ EXCLUDED.seller_address
3165
+ ),
3166
+ buyer_tracked = hyperliquid_whale_trade_events.buyer_tracked OR EXCLUDED.buyer_tracked,
3167
+ seller_tracked = hyperliquid_whale_trade_events.seller_tracked OR EXCLUDED.seller_tracked,
3168
+ buyer_start_position = COALESCE(
3169
+ hyperliquid_whale_trade_events.buyer_start_position,
3170
+ EXCLUDED.buyer_start_position
3171
+ ),
3172
+ buyer_end_position = COALESCE(
3173
+ hyperliquid_whale_trade_events.buyer_end_position,
3174
+ EXCLUDED.buyer_end_position
3175
+ ),
3176
+ buyer_position_action = COALESCE(
3177
+ hyperliquid_whale_trade_events.buyer_position_action,
3178
+ EXCLUDED.buyer_position_action
3179
+ ),
3180
+ buyer_closed_pnl = COALESCE(
3181
+ hyperliquid_whale_trade_events.buyer_closed_pnl,
3182
+ EXCLUDED.buyer_closed_pnl
3183
+ ),
3184
+ buyer_liquidation = COALESCE(
3185
+ hyperliquid_whale_trade_events.buyer_liquidation,
3186
+ EXCLUDED.buyer_liquidation
3187
+ ),
3188
+ seller_start_position = COALESCE(
3189
+ hyperliquid_whale_trade_events.seller_start_position,
3190
+ EXCLUDED.seller_start_position
3191
+ ),
3192
+ seller_end_position = COALESCE(
3193
+ hyperliquid_whale_trade_events.seller_end_position,
3194
+ EXCLUDED.seller_end_position
3195
+ ),
3196
+ seller_position_action = COALESCE(
3197
+ hyperliquid_whale_trade_events.seller_position_action,
3198
+ EXCLUDED.seller_position_action
3199
+ ),
3200
+ seller_closed_pnl = COALESCE(
3201
+ hyperliquid_whale_trade_events.seller_closed_pnl,
3202
+ EXCLUDED.seller_closed_pnl
3203
+ ),
3204
+ seller_liquidation = COALESCE(
3205
+ hyperliquid_whale_trade_events.seller_liquidation,
3206
+ EXCLUDED.seller_liquidation
3207
+ ),
3208
+ source = EXCLUDED.source,
3209
+ ingested_at = now()
3210
+ `,
3211
+ flat
3212
+ );
4040
3213
  }
4041
- async function getDataEdgesForSymbols(provider, symbols, interval) {
4042
- const normalizedSymbols = [
4043
- ...new Set(symbols.map(normalizeCandleSymbol).filter(Boolean))
3214
+ async function upsertHyperliquidWhaleFlowRows(rows) {
3215
+ if (!rows.length) return;
3216
+ await ensureHyperliquidWhaleSchema();
3217
+ const cols = [
3218
+ "symbol",
3219
+ "interval",
3220
+ "ts",
3221
+ "trades",
3222
+ "whale_sides",
3223
+ "unique_whales",
3224
+ "whale_addresses",
3225
+ "buy_notional_usd",
3226
+ "sell_notional_usd",
3227
+ "net_notional_usd",
3228
+ "buy_share_pct",
3229
+ "position_aware_whale_sides",
3230
+ "long_entry_whale_addresses",
3231
+ "short_entry_whale_addresses",
3232
+ "long_exit_whale_addresses",
3233
+ "short_exit_whale_addresses",
3234
+ "long_entry_notional_usd",
3235
+ "short_entry_notional_usd",
3236
+ "long_exit_notional_usd",
3237
+ "short_exit_notional_usd",
3238
+ "entry_net_notional_usd",
3239
+ "entry_long_share_pct",
3240
+ "universe_fingerprint",
3241
+ "whale_registry_fingerprint",
3242
+ "source"
4044
3243
  ];
4045
- const result = /* @__PURE__ */ new Map();
4046
- for (const symbol of normalizedSymbols) {
4047
- result.set(symbol, {});
4048
- }
4049
- if (!normalizedSymbols.length) {
4050
- return result;
3244
+ const maxRows = getSafeBulkInsertRows(cols.length);
3245
+ if (rows.length > maxRows) {
3246
+ for (let index = 0; index < rows.length; index += maxRows) {
3247
+ await upsertHyperliquidWhaleFlowRows(rows.slice(index, index + maxRows));
3248
+ }
3249
+ return;
4051
3250
  }
4052
- await ensureCandlesSchema();
4053
- const pool = getPool();
4054
- const normalizedProvider = normalizeCandleProvider(provider);
4055
- const sql = `
4056
- WITH requested(symbol) AS (
4057
- SELECT unnest($2::text[])
4058
- )
4059
- SELECT
4060
- r.symbol,
4061
- (
4062
- SELECT extract(epoch from c.ts)*1000
4063
- FROM candles c
4064
- WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
4065
- ORDER BY c.ts ASC
4066
- LIMIT 1
4067
- ) AS min_ms,
4068
- (
4069
- SELECT extract(epoch from c.ts)*1000
4070
- FROM candles c
4071
- WHERE c.provider = $1 AND c.symbol = r.symbol AND c.interval = $3
4072
- ORDER BY c.ts DESC
4073
- LIMIT 1
4074
- ) AS max_ms
4075
- FROM requested r
4076
- `;
4077
- const response = await pool.query(sql, [
4078
- normalizedProvider,
4079
- normalizedSymbols,
4080
- interval
3251
+ const valuesSql = rows.map(
3252
+ (_, rowIndex) => `(${cols.map((__, colIndex) => `$${rowIndex * cols.length + colIndex + 1}`).join(",")})`
3253
+ ).join(",");
3254
+ const flat = rows.flatMap((row) => [
3255
+ row.symbol,
3256
+ row.interval,
3257
+ row.ts,
3258
+ row.trades,
3259
+ row.whaleSides,
3260
+ row.uniqueWhales,
3261
+ row.whaleAddresses ?? [],
3262
+ row.buyNotionalUsd,
3263
+ row.sellNotionalUsd,
3264
+ row.netNotionalUsd,
3265
+ row.buySharePct ?? null,
3266
+ row.positionAwareWhaleSides,
3267
+ row.longEntryWhaleAddresses ?? [],
3268
+ row.shortEntryWhaleAddresses ?? [],
3269
+ row.longExitWhaleAddresses ?? [],
3270
+ row.shortExitWhaleAddresses ?? [],
3271
+ row.longEntryNotionalUsd,
3272
+ row.shortEntryNotionalUsd,
3273
+ row.longExitNotionalUsd,
3274
+ row.shortExitNotionalUsd,
3275
+ row.entryNetNotionalUsd,
3276
+ row.entryLongSharePct ?? null,
3277
+ row.universeFingerprint,
3278
+ row.whaleRegistryFingerprint,
3279
+ row.source ?? null
4081
3280
  ]);
4082
- for (const row of response.rows) {
4083
- const symbol = normalizeCandleSymbol(String(row.symbol || ""));
4084
- if (!symbol) continue;
4085
- const min = row.min_ms == null ? NaN : Number(row.min_ms);
4086
- const max = row.max_ms == null ? NaN : Number(row.max_ms);
4087
- result.set(symbol, {
4088
- ...Number.isFinite(min) ? { min } : {},
4089
- ...Number.isFinite(max) ? { max } : {}
3281
+ await getPool().query(
3282
+ `
3283
+ INSERT INTO hyperliquid_whale_flow (${cols.join(",")})
3284
+ VALUES ${valuesSql}
3285
+ ON CONFLICT (
3286
+ universe_fingerprint,
3287
+ whale_registry_fingerprint,
3288
+ symbol,
3289
+ interval,
3290
+ ts
3291
+ ) DO UPDATE SET
3292
+ trades = EXCLUDED.trades,
3293
+ whale_sides = EXCLUDED.whale_sides,
3294
+ unique_whales = EXCLUDED.unique_whales,
3295
+ whale_addresses = EXCLUDED.whale_addresses,
3296
+ buy_notional_usd = EXCLUDED.buy_notional_usd,
3297
+ sell_notional_usd = EXCLUDED.sell_notional_usd,
3298
+ net_notional_usd = EXCLUDED.net_notional_usd,
3299
+ buy_share_pct = EXCLUDED.buy_share_pct,
3300
+ position_aware_whale_sides = EXCLUDED.position_aware_whale_sides,
3301
+ long_entry_whale_addresses = EXCLUDED.long_entry_whale_addresses,
3302
+ short_entry_whale_addresses = EXCLUDED.short_entry_whale_addresses,
3303
+ long_exit_whale_addresses = EXCLUDED.long_exit_whale_addresses,
3304
+ short_exit_whale_addresses = EXCLUDED.short_exit_whale_addresses,
3305
+ long_entry_notional_usd = EXCLUDED.long_entry_notional_usd,
3306
+ short_entry_notional_usd = EXCLUDED.short_entry_notional_usd,
3307
+ long_exit_notional_usd = EXCLUDED.long_exit_notional_usd,
3308
+ short_exit_notional_usd = EXCLUDED.short_exit_notional_usd,
3309
+ entry_net_notional_usd = EXCLUDED.entry_net_notional_usd,
3310
+ entry_long_share_pct = EXCLUDED.entry_long_share_pct,
3311
+ source = EXCLUDED.source,
3312
+ ingested_at = now()
3313
+ `,
3314
+ flat
3315
+ );
3316
+ }
3317
+ async function rebuildHyperliquidWhaleFlowRows(params) {
3318
+ await ensureHyperliquidWhaleSchema();
3319
+ const client = await getPool().connect();
3320
+ try {
3321
+ await client.query("BEGIN");
3322
+ const result = await client.query(
3323
+ `
3324
+ WITH source_events AS (
3325
+ SELECT *
3326
+ FROM hyperliquid_whale_trade_events
3327
+ WHERE universe_fingerprint = $1
3328
+ AND whale_registry_fingerprint = $2
3329
+ AND ts >= to_timestamp($3/1000.0)
3330
+ AND ts < to_timestamp($4/1000.0)
3331
+ ), metrics AS (
3332
+ SELECT
3333
+ symbol,
3334
+ date_trunc('minute', ts) AS bucket_ts,
3335
+ COUNT(*)::int AS trades,
3336
+ SUM(buyer_tracked::int + seller_tracked::int)::int AS whale_sides,
3337
+ SUM(CASE WHEN buyer_tracked THEN notional_usd ELSE 0 END) AS buy_notional_usd,
3338
+ SUM(CASE WHEN seller_tracked THEN notional_usd ELSE 0 END) AS sell_notional_usd
3339
+ FROM source_events
3340
+ GROUP BY symbol, date_trunc('minute', ts)
3341
+ ), position_legs AS (
3342
+ SELECT
3343
+ symbol,
3344
+ ts,
3345
+ price,
3346
+ buyer_address AS whale_address,
3347
+ buyer_start_position AS start_position,
3348
+ buyer_end_position AS end_position
3349
+ FROM source_events
3350
+ WHERE buyer_tracked
3351
+ AND buyer_address IS NOT NULL
3352
+ AND buyer_start_position IS NOT NULL
3353
+ AND buyer_end_position IS NOT NULL
3354
+ UNION ALL
3355
+ SELECT
3356
+ symbol,
3357
+ ts,
3358
+ price,
3359
+ seller_address AS whale_address,
3360
+ seller_start_position AS start_position,
3361
+ seller_end_position AS end_position
3362
+ FROM source_events
3363
+ WHERE seller_tracked
3364
+ AND seller_address IS NOT NULL
3365
+ AND seller_start_position IS NOT NULL
3366
+ AND seller_end_position IS NOT NULL
3367
+ ), classified_legs AS (
3368
+ SELECT
3369
+ *,
3370
+ GREATEST(
3371
+ GREATEST(end_position, 0) - GREATEST(start_position, 0),
3372
+ 0
3373
+ ) AS long_entry_size,
3374
+ GREATEST(
3375
+ GREATEST(-end_position, 0) - GREATEST(-start_position, 0),
3376
+ 0
3377
+ ) AS short_entry_size,
3378
+ GREATEST(
3379
+ GREATEST(start_position, 0) - GREATEST(end_position, 0),
3380
+ 0
3381
+ ) AS long_exit_size,
3382
+ GREATEST(
3383
+ GREATEST(-start_position, 0) - GREATEST(-end_position, 0),
3384
+ 0
3385
+ ) AS short_exit_size
3386
+ FROM position_legs
3387
+ ), position_metrics AS (
3388
+ SELECT
3389
+ symbol,
3390
+ date_trunc('minute', ts) AS bucket_ts,
3391
+ COUNT(*)::int AS position_aware_whale_sides,
3392
+ SUM(long_entry_size * price) AS long_entry_notional_usd,
3393
+ SUM(short_entry_size * price) AS short_entry_notional_usd,
3394
+ SUM(long_exit_size * price) AS long_exit_notional_usd,
3395
+ SUM(short_exit_size * price) AS short_exit_notional_usd,
3396
+ COALESCE(
3397
+ ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
3398
+ FILTER (WHERE long_entry_size > 0),
3399
+ '{}'
3400
+ ) AS long_entry_whale_addresses,
3401
+ COALESCE(
3402
+ ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
3403
+ FILTER (WHERE short_entry_size > 0),
3404
+ '{}'
3405
+ ) AS short_entry_whale_addresses,
3406
+ COALESCE(
3407
+ ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
3408
+ FILTER (WHERE long_exit_size > 0),
3409
+ '{}'
3410
+ ) AS long_exit_whale_addresses,
3411
+ COALESCE(
3412
+ ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address)
3413
+ FILTER (WHERE short_exit_size > 0),
3414
+ '{}'
3415
+ ) AS short_exit_whale_addresses
3416
+ FROM classified_legs
3417
+ GROUP BY symbol, date_trunc('minute', ts)
3418
+ ), addresses AS (
3419
+ SELECT
3420
+ symbol,
3421
+ date_trunc('minute', ts) AS bucket_ts,
3422
+ ARRAY_AGG(DISTINCT whale_address ORDER BY whale_address) AS whale_addresses
3423
+ FROM source_events
3424
+ CROSS JOIN LATERAL UNNEST(ARRAY[
3425
+ CASE WHEN buyer_tracked THEN buyer_address END,
3426
+ CASE WHEN seller_tracked THEN seller_address END
3427
+ ]) AS expanded(whale_address)
3428
+ WHERE whale_address IS NOT NULL
3429
+ GROUP BY symbol, date_trunc('minute', ts)
3430
+ )
3431
+ INSERT INTO hyperliquid_whale_flow (
3432
+ symbol,
3433
+ interval,
3434
+ ts,
3435
+ trades,
3436
+ whale_sides,
3437
+ unique_whales,
3438
+ whale_addresses,
3439
+ buy_notional_usd,
3440
+ sell_notional_usd,
3441
+ net_notional_usd,
3442
+ buy_share_pct,
3443
+ position_aware_whale_sides,
3444
+ long_entry_whale_addresses,
3445
+ short_entry_whale_addresses,
3446
+ long_exit_whale_addresses,
3447
+ short_exit_whale_addresses,
3448
+ long_entry_notional_usd,
3449
+ short_entry_notional_usd,
3450
+ long_exit_notional_usd,
3451
+ short_exit_notional_usd,
3452
+ entry_net_notional_usd,
3453
+ entry_long_share_pct,
3454
+ universe_fingerprint,
3455
+ whale_registry_fingerprint,
3456
+ source
3457
+ )
3458
+ SELECT
3459
+ metrics.symbol,
3460
+ '1m',
3461
+ metrics.bucket_ts,
3462
+ metrics.trades,
3463
+ metrics.whale_sides,
3464
+ COALESCE(CARDINALITY(addresses.whale_addresses), 0),
3465
+ COALESCE(addresses.whale_addresses, '{}'),
3466
+ metrics.buy_notional_usd,
3467
+ metrics.sell_notional_usd,
3468
+ metrics.buy_notional_usd - metrics.sell_notional_usd,
3469
+ CASE
3470
+ WHEN metrics.buy_notional_usd + metrics.sell_notional_usd > 0
3471
+ THEN metrics.buy_notional_usd /
3472
+ (metrics.buy_notional_usd + metrics.sell_notional_usd)
3473
+ ELSE NULL
3474
+ END,
3475
+ COALESCE(position_metrics.position_aware_whale_sides, 0),
3476
+ COALESCE(position_metrics.long_entry_whale_addresses, '{}'),
3477
+ COALESCE(position_metrics.short_entry_whale_addresses, '{}'),
3478
+ COALESCE(position_metrics.long_exit_whale_addresses, '{}'),
3479
+ COALESCE(position_metrics.short_exit_whale_addresses, '{}'),
3480
+ COALESCE(position_metrics.long_entry_notional_usd, 0),
3481
+ COALESCE(position_metrics.short_entry_notional_usd, 0),
3482
+ COALESCE(position_metrics.long_exit_notional_usd, 0),
3483
+ COALESCE(position_metrics.short_exit_notional_usd, 0),
3484
+ COALESCE(position_metrics.long_entry_notional_usd, 0) -
3485
+ COALESCE(position_metrics.short_entry_notional_usd, 0),
3486
+ CASE
3487
+ WHEN COALESCE(position_metrics.long_entry_notional_usd, 0) +
3488
+ COALESCE(position_metrics.short_entry_notional_usd, 0) > 0
3489
+ THEN COALESCE(position_metrics.long_entry_notional_usd, 0) /
3490
+ (
3491
+ COALESCE(position_metrics.long_entry_notional_usd, 0) +
3492
+ COALESCE(position_metrics.short_entry_notional_usd, 0)
3493
+ )
3494
+ ELSE NULL
3495
+ END,
3496
+ $1,
3497
+ $2,
3498
+ CASE
3499
+ WHEN COALESCE(position_metrics.position_aware_whale_sides, 0) > 0
3500
+ THEN 'hyperliquid_user_fills'
3501
+ ELSE 'hyperliquid_trades'
3502
+ END
3503
+ FROM metrics
3504
+ LEFT JOIN addresses USING (symbol, bucket_ts)
3505
+ LEFT JOIN position_metrics USING (symbol, bucket_ts)
3506
+ ON CONFLICT (
3507
+ universe_fingerprint,
3508
+ whale_registry_fingerprint,
3509
+ symbol,
3510
+ interval,
3511
+ ts
3512
+ ) DO UPDATE SET
3513
+ trades = EXCLUDED.trades,
3514
+ whale_sides = EXCLUDED.whale_sides,
3515
+ unique_whales = EXCLUDED.unique_whales,
3516
+ whale_addresses = EXCLUDED.whale_addresses,
3517
+ buy_notional_usd = EXCLUDED.buy_notional_usd,
3518
+ sell_notional_usd = EXCLUDED.sell_notional_usd,
3519
+ net_notional_usd = EXCLUDED.net_notional_usd,
3520
+ buy_share_pct = EXCLUDED.buy_share_pct,
3521
+ position_aware_whale_sides = EXCLUDED.position_aware_whale_sides,
3522
+ long_entry_whale_addresses = EXCLUDED.long_entry_whale_addresses,
3523
+ short_entry_whale_addresses = EXCLUDED.short_entry_whale_addresses,
3524
+ long_exit_whale_addresses = EXCLUDED.long_exit_whale_addresses,
3525
+ short_exit_whale_addresses = EXCLUDED.short_exit_whale_addresses,
3526
+ long_entry_notional_usd = EXCLUDED.long_entry_notional_usd,
3527
+ short_entry_notional_usd = EXCLUDED.short_entry_notional_usd,
3528
+ long_exit_notional_usd = EXCLUDED.long_exit_notional_usd,
3529
+ short_exit_notional_usd = EXCLUDED.short_exit_notional_usd,
3530
+ entry_net_notional_usd = EXCLUDED.entry_net_notional_usd,
3531
+ entry_long_share_pct = EXCLUDED.entry_long_share_pct,
3532
+ source = EXCLUDED.source,
3533
+ ingested_at = now()
3534
+ RETURNING 1
3535
+ `,
3536
+ [
3537
+ params.universeFingerprint,
3538
+ params.whaleRegistryFingerprint,
3539
+ params.fromMs,
3540
+ params.toMs
3541
+ ]
3542
+ );
3543
+ if (params.deleteEventsBeforeMs != null) {
3544
+ await client.query(
3545
+ `
3546
+ DELETE FROM hyperliquid_whale_trade_events
3547
+ WHERE universe_fingerprint = $1
3548
+ AND whale_registry_fingerprint = $2
3549
+ AND ts < to_timestamp($3/1000.0)
3550
+ `,
3551
+ [
3552
+ params.universeFingerprint,
3553
+ params.whaleRegistryFingerprint,
3554
+ params.deleteEventsBeforeMs
3555
+ ]
3556
+ );
3557
+ }
3558
+ await client.query("COMMIT");
3559
+ return result.rowCount ?? 0;
3560
+ } catch (error) {
3561
+ await client.query("ROLLBACK");
3562
+ throw error;
3563
+ } finally {
3564
+ client.release();
3565
+ }
3566
+ }
3567
+ async function getHyperliquidWhaleWalletCoverage(params) {
3568
+ await ensureHyperliquidWhaleSchema();
3569
+ const result = await getPool().query(
3570
+ `
3571
+ SELECT
3572
+ status,
3573
+ covered_from_ts,
3574
+ covered_to_ts,
3575
+ fills_count,
3576
+ error,
3577
+ checked_at
3578
+ FROM hyperliquid_whale_wallet_coverage
3579
+ WHERE universe_fingerprint = $1
3580
+ AND whale_registry_fingerprint = $2
3581
+ AND address = $3
3582
+ AND data_model_version = $6
3583
+ AND (
3584
+ (
3585
+ covered_from_ts <= to_timestamp($4/1000.0)
3586
+ AND covered_to_ts >= to_timestamp($5/1000.0)
3587
+ )
3588
+ OR (
3589
+ requested_from_ts = to_timestamp($4/1000.0)
3590
+ AND requested_to_ts = to_timestamp($5/1000.0)
3591
+ )
3592
+ )
3593
+ ORDER BY checked_at DESC
3594
+ LIMIT 1
3595
+ `,
3596
+ [
3597
+ params.universeFingerprint,
3598
+ params.whaleRegistryFingerprint,
3599
+ params.address.toLowerCase(),
3600
+ params.fromMs,
3601
+ params.toMs,
3602
+ import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3603
+ ]
3604
+ );
3605
+ const row = result.rows[0];
3606
+ if (!row) return null;
3607
+ return {
3608
+ status: row.covered_from_ts != null && row.covered_to_ts != null && new Date(row.covered_from_ts).getTime() <= params.fromMs && new Date(row.covered_to_ts).getTime() >= params.toMs ? "complete" : String(row.status),
3609
+ coveredFromMs: row.covered_from_ts == null ? null : new Date(row.covered_from_ts).getTime(),
3610
+ coveredToMs: row.covered_to_ts == null ? null : new Date(row.covered_to_ts).getTime(),
3611
+ fillsCount: Number(row.fills_count) || 0,
3612
+ error: row.error == null ? null : String(row.error),
3613
+ checkedAt: new Date(row.checked_at)
3614
+ };
3615
+ }
3616
+ async function upsertHyperliquidWhaleWalletCoverage(params) {
3617
+ await ensureHyperliquidWhaleSchema();
3618
+ await getPool().query(
3619
+ `
3620
+ INSERT INTO hyperliquid_whale_wallet_coverage (
3621
+ universe_fingerprint,
3622
+ whale_registry_fingerprint,
3623
+ address,
3624
+ requested_from_ts,
3625
+ requested_to_ts,
3626
+ covered_from_ts,
3627
+ covered_to_ts,
3628
+ status,
3629
+ fills_count,
3630
+ error,
3631
+ data_model_version
3632
+ ) VALUES (
3633
+ $1,
3634
+ $2,
3635
+ $3,
3636
+ to_timestamp($4/1000.0),
3637
+ to_timestamp($5/1000.0),
3638
+ CASE WHEN $6::double precision IS NULL THEN NULL ELSE to_timestamp($6/1000.0) END,
3639
+ CASE WHEN $7::double precision IS NULL THEN NULL ELSE to_timestamp($7/1000.0) END,
3640
+ $8,
3641
+ $9,
3642
+ $10,
3643
+ $11
3644
+ )
3645
+ ON CONFLICT (
3646
+ universe_fingerprint,
3647
+ whale_registry_fingerprint,
3648
+ address,
3649
+ requested_from_ts,
3650
+ requested_to_ts
3651
+ ) DO UPDATE SET
3652
+ covered_from_ts = EXCLUDED.covered_from_ts,
3653
+ covered_to_ts = EXCLUDED.covered_to_ts,
3654
+ status = EXCLUDED.status,
3655
+ fills_count = EXCLUDED.fills_count,
3656
+ error = EXCLUDED.error,
3657
+ data_model_version = EXCLUDED.data_model_version,
3658
+ checked_at = now()
3659
+ `,
3660
+ [
3661
+ params.universeFingerprint,
3662
+ params.whaleRegistryFingerprint,
3663
+ params.address.toLowerCase(),
3664
+ params.fromMs,
3665
+ params.toMs,
3666
+ params.coveredFromMs,
3667
+ params.coveredToMs,
3668
+ params.status,
3669
+ params.fillsCount,
3670
+ params.error ?? null,
3671
+ import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3672
+ ]
3673
+ );
3674
+ }
3675
+ async function rebuildHyperliquidWhaleCoverageRows(params) {
3676
+ await ensureHyperliquidWhaleSchema();
3677
+ if (params.toMs <= params.fromMs) return 0;
3678
+ const minuteMs = 6e4;
3679
+ const defaultChunkMinutes = 7 * 24 * 60;
3680
+ const chunkMinutes = Number.isFinite(params.chunkMinutes) && Number(params.chunkMinutes) > 0 ? Math.floor(Number(params.chunkMinutes)) : defaultChunkMinutes;
3681
+ const chunkMs = chunkMinutes * minuteMs;
3682
+ const totalBuckets = Math.ceil((params.toMs - params.fromMs) / minuteMs);
3683
+ const totalChunks = Math.ceil((params.toMs - params.fromMs) / chunkMs);
3684
+ let completedBuckets = 0;
3685
+ let rows = 0;
3686
+ for (let chunkIndex = 0, chunkFromMs = params.fromMs; chunkFromMs < params.toMs; chunkIndex += 1, chunkFromMs += chunkMs) {
3687
+ const chunkToMs = Math.min(params.toMs, chunkFromMs + chunkMs);
3688
+ const result = await getPool().query(
3689
+ `
3690
+ WITH normalized_ranges AS (
3691
+ SELECT
3692
+ address,
3693
+ GREATEST(
3694
+ to_timestamp($3/1000.0),
3695
+ date_trunc('minute', covered_from_ts) +
3696
+ CASE
3697
+ WHEN covered_from_ts = date_trunc('minute', covered_from_ts)
3698
+ THEN interval '0 minutes'
3699
+ ELSE interval '1 minute'
3700
+ END
3701
+ ) AS range_start,
3702
+ LEAST(
3703
+ to_timestamp($4/1000.0),
3704
+ date_trunc('minute', covered_to_ts)
3705
+ ) AS range_end
3706
+ FROM hyperliquid_whale_wallet_coverage
3707
+ WHERE universe_fingerprint = $1
3708
+ AND whale_registry_fingerprint = $2
3709
+ AND data_model_version = $6
3710
+ AND status IN ('complete', 'truncated')
3711
+ AND covered_from_ts < to_timestamp($4/1000.0)
3712
+ AND covered_to_ts > to_timestamp($3/1000.0)
3713
+ ), eligible_ranges AS (
3714
+ SELECT *
3715
+ FROM normalized_ranges
3716
+ WHERE range_start < range_end
3717
+ ), ordered_ranges AS (
3718
+ SELECT
3719
+ *,
3720
+ MAX(range_end) OVER (
3721
+ PARTITION BY address
3722
+ ORDER BY range_start, range_end
3723
+ ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
3724
+ ) AS previous_max_end
3725
+ FROM eligible_ranges
3726
+ ), marked_ranges AS (
3727
+ SELECT
3728
+ *,
3729
+ SUM(
3730
+ CASE
3731
+ WHEN previous_max_end IS NULL OR range_start > previous_max_end
3732
+ THEN 1
3733
+ ELSE 0
3734
+ END
3735
+ ) OVER (
3736
+ PARTITION BY address
3737
+ ORDER BY range_start, range_end
3738
+ ) AS range_group
3739
+ FROM ordered_ranges
3740
+ ), merged_ranges AS (
3741
+ SELECT
3742
+ address,
3743
+ MIN(range_start) AS range_start,
3744
+ MAX(range_end) AS range_end
3745
+ FROM marked_ranges
3746
+ GROUP BY address, range_group
3747
+ ), deltas AS (
3748
+ SELECT range_start AS ts, 1 AS delta
3749
+ FROM merged_ranges
3750
+ UNION ALL
3751
+ SELECT range_end AS ts, -1 AS delta
3752
+ FROM merged_ranges
3753
+ ), bucket_deltas AS (
3754
+ SELECT ts, SUM(delta)::int AS delta
3755
+ FROM deltas
3756
+ GROUP BY ts
3757
+ ), buckets AS (
3758
+ SELECT generate_series(
3759
+ to_timestamp($3/1000.0),
3760
+ to_timestamp($4/1000.0) - interval '1 minute',
3761
+ interval '1 minute'
3762
+ ) AS ts
3763
+ ), coverage AS (
3764
+ SELECT
3765
+ buckets.ts,
3766
+ SUM(COALESCE(bucket_deltas.delta, 0)) OVER (
3767
+ ORDER BY buckets.ts
3768
+ ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
3769
+ )::int AS covered_whales
3770
+ FROM buckets
3771
+ LEFT JOIN bucket_deltas USING (ts)
3772
+ )
3773
+ INSERT INTO hyperliquid_whale_coverage_1m (
3774
+ ts,
3775
+ covered_whales,
3776
+ expected_whales,
3777
+ coverage_pct,
3778
+ universe_fingerprint,
3779
+ whale_registry_fingerprint,
3780
+ source,
3781
+ data_model_version
3782
+ )
3783
+ SELECT
3784
+ ts,
3785
+ covered_whales,
3786
+ $5,
3787
+ CASE WHEN $5 > 0 THEN covered_whales::double precision / $5 ELSE 0 END,
3788
+ $1,
3789
+ $2,
3790
+ 'hyperliquid_user_fills',
3791
+ $6
3792
+ FROM coverage
3793
+ ON CONFLICT (
3794
+ universe_fingerprint,
3795
+ whale_registry_fingerprint,
3796
+ ts
3797
+ ) DO UPDATE SET
3798
+ covered_whales = EXCLUDED.covered_whales,
3799
+ expected_whales = EXCLUDED.expected_whales,
3800
+ coverage_pct = EXCLUDED.coverage_pct,
3801
+ source = EXCLUDED.source,
3802
+ data_model_version = EXCLUDED.data_model_version,
3803
+ ingested_at = now()
3804
+ `,
3805
+ [
3806
+ params.universeFingerprint,
3807
+ params.whaleRegistryFingerprint,
3808
+ chunkFromMs,
3809
+ chunkToMs,
3810
+ params.expectedWhales,
3811
+ import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3812
+ ]
3813
+ );
3814
+ const chunkBuckets = Math.ceil((chunkToMs - chunkFromMs) / minuteMs);
3815
+ completedBuckets = Math.min(totalBuckets, completedBuckets + chunkBuckets);
3816
+ rows += result.rowCount ?? 0;
3817
+ params.onProgress?.({
3818
+ chunkIndex: chunkIndex + 1,
3819
+ totalChunks,
3820
+ completedBuckets,
3821
+ totalBuckets,
3822
+ rows
4090
3823
  });
4091
3824
  }
4092
- return result;
3825
+ return rows;
4093
3826
  }
4094
- async function waitForDbReady(attempts = 20, delayMs = 1e3) {
4095
- const pool = getPool();
4096
- let lastError;
4097
- for (let i = 0; i < attempts; i++) {
4098
- try {
4099
- await pool.query("SELECT 1");
4100
- return;
4101
- } catch (e) {
4102
- lastError = e;
4103
- await new Promise((r) => setTimeout(r, delayMs));
3827
+ async function upsertHyperliquidWhaleCoverageRows(rows) {
3828
+ if (!rows.length) return;
3829
+ await ensureHyperliquidWhaleSchema();
3830
+ const cols = [
3831
+ "ts",
3832
+ "covered_whales",
3833
+ "expected_whales",
3834
+ "coverage_pct",
3835
+ "universe_fingerprint",
3836
+ "whale_registry_fingerprint",
3837
+ "source",
3838
+ "data_model_version"
3839
+ ];
3840
+ const maxRows = getSafeBulkInsertRows(cols.length);
3841
+ if (rows.length > maxRows) {
3842
+ for (let index = 0; index < rows.length; index += maxRows) {
3843
+ await upsertHyperliquidWhaleCoverageRows(
3844
+ rows.slice(index, index + maxRows)
3845
+ );
4104
3846
  }
3847
+ return;
4105
3848
  }
4106
- throw lastError;
3849
+ const values = [];
3850
+ const tuples = rows.map((row, rowIndex) => {
3851
+ const offset = rowIndex * cols.length;
3852
+ values.push(
3853
+ row.ts,
3854
+ row.coveredWhales,
3855
+ row.expectedWhales,
3856
+ row.coveragePct,
3857
+ row.universeFingerprint,
3858
+ row.whaleRegistryFingerprint,
3859
+ row.source ?? null,
3860
+ row.dataModelVersion ?? import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
3861
+ );
3862
+ return `(${cols.map((_, colIndex) => `$${offset + colIndex + 1}`).join(",")})`;
3863
+ });
3864
+ await getPool().query(
3865
+ `
3866
+ INSERT INTO hyperliquid_whale_coverage_1m (${cols.join(",")})
3867
+ VALUES ${tuples.join(",")}
3868
+ ON CONFLICT (
3869
+ universe_fingerprint,
3870
+ whale_registry_fingerprint,
3871
+ ts
3872
+ ) DO UPDATE SET
3873
+ covered_whales = EXCLUDED.covered_whales,
3874
+ expected_whales = EXCLUDED.expected_whales,
3875
+ coverage_pct = EXCLUDED.coverage_pct,
3876
+ source = EXCLUDED.source,
3877
+ data_model_version = EXCLUDED.data_model_version,
3878
+ ingested_at = now()
3879
+ `,
3880
+ values
3881
+ );
4107
3882
  }
4108
- async function deleteCandles(provider, symbol, interval) {
4109
- const pool = getPool();
4110
- const normalizedProvider = normalizeCandleProvider(provider);
4111
- const normalizedSymbol = normalizeCandleSymbol(symbol);
4112
- const sql = `
4113
- DELETE FROM candles
4114
- WHERE provider = $1 AND symbol = $2 AND interval = $3
4115
- `;
4116
- await pool.query(sql, [normalizedProvider, normalizedSymbol, interval]);
3883
+ async function getHyperliquidWhaleCoverageSeriesRows(params) {
3884
+ await prepareMarketContextSchemaForRead("hyperliquidWhales");
3885
+ const result = await queryMarketContext(
3886
+ `
3887
+ SELECT
3888
+ ts,
3889
+ covered_whales,
3890
+ expected_whales,
3891
+ coverage_pct
3892
+ FROM hyperliquid_whale_coverage_1m
3893
+ WHERE universe_fingerprint = $1
3894
+ AND whale_registry_fingerprint = $2
3895
+ AND data_model_version = $3
3896
+ AND ts >= to_timestamp($4/1000.0)
3897
+ AND ts < to_timestamp($5/1000.0)
3898
+ ORDER BY ts
3899
+ `,
3900
+ [
3901
+ params.universeFingerprint,
3902
+ params.whaleRegistryFingerprint,
3903
+ import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION,
3904
+ params.fromMs,
3905
+ params.toMs
3906
+ ],
3907
+ params
3908
+ );
3909
+ return result.rows.map((row) => ({
3910
+ ts: new Date(row.ts),
3911
+ coveredWhales: Number(row.covered_whales) || 0,
3912
+ expectedWhales: Number(row.expected_whales) || 0,
3913
+ coveragePct: Number(row.coverage_pct) || 0
3914
+ }));
3915
+ }
3916
+ async function getHyperliquidWhaleFlowSeriesRows(params) {
3917
+ await prepareMarketContextSchemaForRead("hyperliquidWhales");
3918
+ const result = await queryMarketContext(
3919
+ `
3920
+ SELECT
3921
+ ts,
3922
+ trades,
3923
+ whale_sides,
3924
+ whale_addresses,
3925
+ buy_notional_usd,
3926
+ sell_notional_usd,
3927
+ position_aware_whale_sides,
3928
+ long_entry_whale_addresses,
3929
+ short_entry_whale_addresses,
3930
+ long_exit_whale_addresses,
3931
+ short_exit_whale_addresses,
3932
+ long_entry_notional_usd,
3933
+ short_entry_notional_usd,
3934
+ long_exit_notional_usd,
3935
+ short_exit_notional_usd
3936
+ FROM hyperliquid_whale_flow
3937
+ WHERE symbol = $1
3938
+ AND interval = '1m'
3939
+ AND universe_fingerprint = $2
3940
+ AND whale_registry_fingerprint = $3
3941
+ AND ts >= to_timestamp($4/1000.0)
3942
+ AND ts < to_timestamp($5/1000.0)
3943
+ ORDER BY ts
3944
+ `,
3945
+ [
3946
+ params.symbol,
3947
+ params.universeFingerprint,
3948
+ params.whaleRegistryFingerprint,
3949
+ params.fromMs,
3950
+ params.toMs
3951
+ ],
3952
+ params
3953
+ );
3954
+ return result.rows.map((row) => ({
3955
+ ts: new Date(row.ts),
3956
+ trades: Number(row.trades) || 0,
3957
+ whaleSides: Number(row.whale_sides) || 0,
3958
+ whaleAddresses: Array.isArray(row.whale_addresses) ? row.whale_addresses.map(String) : [],
3959
+ buyNotionalUsd: Number(row.buy_notional_usd) || 0,
3960
+ sellNotionalUsd: Number(row.sell_notional_usd) || 0,
3961
+ positionAwareWhaleSides: Number(row.position_aware_whale_sides) || 0,
3962
+ longEntryWhaleAddresses: Array.isArray(row.long_entry_whale_addresses) ? row.long_entry_whale_addresses.map(String) : [],
3963
+ shortEntryWhaleAddresses: Array.isArray(row.short_entry_whale_addresses) ? row.short_entry_whale_addresses.map(String) : [],
3964
+ longExitWhaleAddresses: Array.isArray(row.long_exit_whale_addresses) ? row.long_exit_whale_addresses.map(String) : [],
3965
+ shortExitWhaleAddresses: Array.isArray(row.short_exit_whale_addresses) ? row.short_exit_whale_addresses.map(String) : [],
3966
+ longEntryNotionalUsd: Number(row.long_entry_notional_usd) || 0,
3967
+ shortEntryNotionalUsd: Number(row.short_entry_notional_usd) || 0,
3968
+ longExitNotionalUsd: Number(row.long_exit_notional_usd) || 0,
3969
+ shortExitNotionalUsd: Number(row.short_exit_notional_usd) || 0
3970
+ }));
4117
3971
  }
4118
- async function findContinuityGap(provider, symbol, interval) {
4119
- const pool = getPool();
4120
- const normalizedProvider = normalizeCandleProvider(provider);
4121
- const normalizedSymbol = normalizeCandleSymbol(symbol);
4122
- const expectedSeconds = interval * 60;
4123
- const sql = `
4124
- WITH ordered AS (
3972
+ async function getHyperliquidWhaleFlowAggregate(params) {
3973
+ await prepareMarketContextSchemaForRead("hyperliquidWhales");
3974
+ const intervalMs = HYPERLIQUID_CONTEXT_INTERVAL_MS[params.interval];
3975
+ const expectedBuckets = Math.ceil(intervalMs / 6e4);
3976
+ const res = await queryMarketContext(
3977
+ `
3978
+ WITH coverage_rows AS (
3979
+ SELECT *
3980
+ FROM hyperliquid_whale_coverage_1m
3981
+ WHERE universe_fingerprint = $2
3982
+ AND whale_registry_fingerprint = $3
3983
+ AND data_model_version = $6
3984
+ AND ts >= to_timestamp(
3985
+ ($4::double precision - $5::double precision) / 1000.0
3986
+ )
3987
+ AND ts < to_timestamp($4/1000.0)
3988
+ ), coverage_summary AS (
3989
+ SELECT
3990
+ COUNT(*)::int AS coverage_buckets,
3991
+ MAX(ts) AS coverage_as_of_ts,
3992
+ MIN(covered_whales)::int AS covered_whales,
3993
+ MAX(expected_whales)::int AS expected_whales,
3994
+ MIN(coverage_pct) AS coverage_pct
3995
+ FROM coverage_rows
3996
+ ), window_rows AS (
3997
+ SELECT *
3998
+ FROM hyperliquid_whale_flow
3999
+ WHERE symbol = $1
4000
+ AND interval = '1m'
4001
+ AND universe_fingerprint = $2
4002
+ AND whale_registry_fingerprint = $3
4003
+ AND ts >= to_timestamp(
4004
+ ($4::double precision - $5::double precision) / 1000.0
4005
+ )
4006
+ AND ts < to_timestamp($4/1000.0)
4007
+ ), unique_addresses AS (
4008
+ SELECT COUNT(DISTINCT address)::int AS unique_whales
4009
+ FROM window_rows
4010
+ CROSS JOIN LATERAL UNNEST(whale_addresses) AS expanded(address)
4011
+ ), directional_counts AS (
4012
+ SELECT
4013
+ (
4014
+ SELECT COUNT(DISTINCT address)::int
4015
+ FROM window_rows
4016
+ CROSS JOIN LATERAL UNNEST(long_entry_whale_addresses) AS expanded(address)
4017
+ ) AS long_entry_whales,
4018
+ (
4019
+ SELECT COUNT(DISTINCT address)::int
4020
+ FROM window_rows
4021
+ CROSS JOIN LATERAL UNNEST(short_entry_whale_addresses) AS expanded(address)
4022
+ ) AS short_entry_whales,
4023
+ (
4024
+ SELECT COUNT(DISTINCT address)::int
4025
+ FROM window_rows
4026
+ CROSS JOIN LATERAL UNNEST(long_exit_whale_addresses) AS expanded(address)
4027
+ ) AS long_exit_whales,
4028
+ (
4029
+ SELECT COUNT(DISTINCT address)::int
4030
+ FROM window_rows
4031
+ CROSS JOIN LATERAL UNNEST(short_exit_whale_addresses) AS expanded(address)
4032
+ ) AS short_exit_whales
4033
+ )
4125
4034
  SELECT
4126
- ts,
4127
- LAG(ts) OVER (ORDER BY ts) AS prev_ts
4128
- FROM candles
4129
- WHERE provider = $1 AND symbol = $2 AND interval = $3
4130
- )
4131
- SELECT
4132
- ts,
4133
- prev_ts,
4134
- EXTRACT(EPOCH FROM (ts - prev_ts))::int AS diff_seconds
4135
- FROM ordered
4136
- WHERE prev_ts IS NOT NULL
4137
- AND EXTRACT(EPOCH FROM (ts - prev_ts))::int <> $4
4138
- ORDER BY ts ASC
4139
- LIMIT 1
4140
- `;
4141
- const res = await pool.query(sql, [
4142
- normalizedProvider,
4143
- normalizedSymbol,
4144
- interval,
4145
- expectedSeconds
4146
- ]);
4035
+ $1::text AS symbol,
4036
+ coverage_summary.coverage_as_of_ts AS as_of_ts,
4037
+ coverage_summary.coverage_buckets,
4038
+ coverage_summary.covered_whales,
4039
+ coverage_summary.expected_whales,
4040
+ coverage_summary.coverage_pct,
4041
+ COALESCE((SELECT SUM(trades) FROM window_rows), 0)::int AS trades,
4042
+ COALESCE((SELECT SUM(whale_sides) FROM window_rows), 0)::int AS whale_sides,
4043
+ COALESCE((SELECT unique_whales FROM unique_addresses), 0)::int AS unique_whales,
4044
+ COALESCE((SELECT SUM(buy_notional_usd) FROM window_rows), 0) AS buy_notional_usd,
4045
+ COALESCE((SELECT SUM(sell_notional_usd) FROM window_rows), 0) AS sell_notional_usd,
4046
+ COALESCE((SELECT SUM(net_notional_usd) FROM window_rows), 0) AS net_notional_usd,
4047
+ CASE
4048
+ WHEN COALESCE((SELECT SUM(buy_notional_usd + sell_notional_usd) FROM window_rows), 0) > 0
4049
+ THEN (SELECT SUM(buy_notional_usd) FROM window_rows) /
4050
+ (SELECT SUM(buy_notional_usd + sell_notional_usd) FROM window_rows)
4051
+ ELSE NULL
4052
+ END AS buy_share_pct,
4053
+ COALESCE((SELECT SUM(position_aware_whale_sides) FROM window_rows), 0)::int
4054
+ AS position_aware_whale_sides,
4055
+ CASE
4056
+ WHEN COALESCE((SELECT SUM(whale_sides) FROM window_rows), 0) > 0
4057
+ THEN COALESCE((SELECT SUM(position_aware_whale_sides) FROM window_rows), 0)::double precision /
4058
+ (SELECT SUM(whale_sides) FROM window_rows)
4059
+ ELSE 0
4060
+ END AS position_aware_pct,
4061
+ COALESCE((SELECT long_entry_whales FROM directional_counts), 0)::int AS long_entry_whales,
4062
+ COALESCE((SELECT short_entry_whales FROM directional_counts), 0)::int AS short_entry_whales,
4063
+ COALESCE((SELECT long_exit_whales FROM directional_counts), 0)::int AS long_exit_whales,
4064
+ COALESCE((SELECT short_exit_whales FROM directional_counts), 0)::int AS short_exit_whales,
4065
+ COALESCE((SELECT SUM(long_entry_notional_usd) FROM window_rows), 0)
4066
+ AS long_entry_notional_usd,
4067
+ COALESCE((SELECT SUM(short_entry_notional_usd) FROM window_rows), 0)
4068
+ AS short_entry_notional_usd,
4069
+ COALESCE((SELECT SUM(long_exit_notional_usd) FROM window_rows), 0)
4070
+ AS long_exit_notional_usd,
4071
+ COALESCE((SELECT SUM(short_exit_notional_usd) FROM window_rows), 0)
4072
+ AS short_exit_notional_usd,
4073
+ COALESCE((SELECT SUM(entry_net_notional_usd) FROM window_rows), 0)
4074
+ AS entry_net_notional_usd,
4075
+ CASE
4076
+ WHEN COALESCE((SELECT SUM(long_entry_notional_usd + short_entry_notional_usd) FROM window_rows), 0) > 0
4077
+ THEN (SELECT SUM(long_entry_notional_usd) FROM window_rows) /
4078
+ (SELECT SUM(long_entry_notional_usd + short_entry_notional_usd) FROM window_rows)
4079
+ ELSE NULL
4080
+ END AS entry_long_share_pct,
4081
+ (SELECT MAX(source) FROM window_rows) AS source
4082
+ FROM coverage_summary
4083
+ `,
4084
+ [
4085
+ params.symbol,
4086
+ params.universeFingerprint,
4087
+ params.whaleRegistryFingerprint,
4088
+ params.decisionTimeMs,
4089
+ intervalMs,
4090
+ import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
4091
+ ],
4092
+ params
4093
+ );
4147
4094
  const row = res.rows[0];
4148
- if (!row) return null;
4095
+ if (!row?.as_of_ts || Number(row.coverage_buckets) !== expectedBuckets || Number(row.covered_whales) <= 0) {
4096
+ return null;
4097
+ }
4098
+ const asOfTs = new Date(row.as_of_ts);
4099
+ const ageMs = params.decisionTimeMs - (asOfTs.getTime() + 6e4);
4149
4100
  return {
4150
- ts: new Date(row.ts).getTime(),
4151
- prevTs: new Date(row.prev_ts).getTime(),
4152
- diffSeconds: row.diff_seconds
4101
+ symbol: params.symbol,
4102
+ interval: params.interval,
4103
+ asOfTs,
4104
+ windowEndTs: new Date(params.decisionTimeMs),
4105
+ trades: Number(row.trades) || 0,
4106
+ whaleSides: Number(row.whale_sides) || 0,
4107
+ uniqueWhales: Number(row.unique_whales) || 0,
4108
+ coveredWhales: Number(row.covered_whales) || 0,
4109
+ expectedWhales: Number(row.expected_whales) || 0,
4110
+ coveragePct: Number(row.coverage_pct) || 0,
4111
+ buyNotionalUsd: Number(row.buy_notional_usd) || 0,
4112
+ sellNotionalUsd: Number(row.sell_notional_usd) || 0,
4113
+ netNotionalUsd: Number(row.net_notional_usd) || 0,
4114
+ buySharePct: row.buy_share_pct == null ? null : Number(row.buy_share_pct) || 0,
4115
+ positionAwareWhaleSides: Number(row.position_aware_whale_sides) || 0,
4116
+ positionAwarePct: Number(row.position_aware_pct) || 0,
4117
+ longEntryWhales: Number(row.long_entry_whales) || 0,
4118
+ shortEntryWhales: Number(row.short_entry_whales) || 0,
4119
+ longExitWhales: Number(row.long_exit_whales) || 0,
4120
+ shortExitWhales: Number(row.short_exit_whales) || 0,
4121
+ longEntryNotionalUsd: Number(row.long_entry_notional_usd) || 0,
4122
+ shortEntryNotionalUsd: Number(row.short_entry_notional_usd) || 0,
4123
+ longExitNotionalUsd: Number(row.long_exit_notional_usd) || 0,
4124
+ shortExitNotionalUsd: Number(row.short_exit_notional_usd) || 0,
4125
+ entryNetNotionalUsd: Number(row.entry_net_notional_usd) || 0,
4126
+ entryLongSharePct: row.entry_long_share_pct == null ? null : Number(row.entry_long_share_pct) || 0,
4127
+ universeFingerprint: params.universeFingerprint,
4128
+ whaleRegistryFingerprint: params.whaleRegistryFingerprint,
4129
+ source: row.source == null ? null : String(row.source),
4130
+ ageMs,
4131
+ stale: ageMs < 0 || (params.maxAgeMs != null && Number.isFinite(params.maxAgeMs) ? ageMs > params.maxAgeMs : false)
4153
4132
  };
4154
4133
  }
4134
+ async function hasHyperliquidWhaleBackfillCoverage(params) {
4135
+ await ensureHyperliquidWhaleSchema();
4136
+ const expectedBuckets = Math.max(
4137
+ 0,
4138
+ Math.ceil((params.toMs - params.fromMs) / 6e4)
4139
+ );
4140
+ const result = await getPool().query(
4141
+ `
4142
+ SELECT
4143
+ COUNT(*)::int AS buckets,
4144
+ COUNT(*) FILTER (
4145
+ WHERE covered_whales = expected_whales
4146
+ )::int AS complete_buckets
4147
+ FROM hyperliquid_whale_coverage_1m
4148
+ WHERE universe_fingerprint = $1
4149
+ AND whale_registry_fingerprint = $2
4150
+ AND data_model_version = $5
4151
+ AND ts >= to_timestamp($3/1000.0)
4152
+ AND ts < to_timestamp($4/1000.0)
4153
+ `,
4154
+ [
4155
+ params.universeFingerprint,
4156
+ params.whaleRegistryFingerprint,
4157
+ params.fromMs,
4158
+ params.toMs,
4159
+ import_types.HYPERLIQUID_WHALE_DATA_MODEL_VERSION
4160
+ ]
4161
+ );
4162
+ return Number(result.rows[0]?.buckets) === expectedBuckets && Number(result.rows[0]?.complete_buckets) === expectedBuckets;
4163
+ }
4155
4164
  // Annotate the CommonJS export names for ESM import in node:
4156
4165
  0 && (module.exports = {
4157
4166
  applyDerivativesMetricCoverage,