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