@tradejs/infra 1.0.0

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.
@@ -0,0 +1,508 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/timescale.ts
21
+ var timescale_exports = {};
22
+ __export(timescale_exports, {
23
+ deleteCandles: () => deleteCandles,
24
+ findContinuityGap: () => findContinuityGap,
25
+ getCandlesRange: () => getCandlesRange,
26
+ getDataEdges: () => getDataEdges,
27
+ getDerivativesRangeForSymbols: () => getDerivativesRangeForSymbols,
28
+ getDerivativesSummary: () => getDerivativesSummary,
29
+ getSpreadRangeForSymbols: () => getSpreadRangeForSymbols,
30
+ getSpreadSummary: () => getSpreadSummary,
31
+ toRows: () => toRows,
32
+ upsertCandles: () => upsertCandles,
33
+ upsertDerivatives: () => upsertDerivatives,
34
+ upsertSpreadRows: () => upsertSpreadRows,
35
+ waitForDbReady: () => waitForDbReady
36
+ });
37
+ module.exports = __toCommonJS(timescale_exports);
38
+ var import_pg = require("pg");
39
+ var getPool = () => {
40
+ if (!global.__pgPool__) {
41
+ const host = process.env.PG_HOST || "127.0.0.1";
42
+ const port = Number(process.env.PG_PORT ?? 5432);
43
+ const user = process.env.PG_USER || "app";
44
+ const password = String(process.env.PG_PASSWORD ?? "app");
45
+ const database = process.env.PG_DATABASE || process.env.PG_DB || "app";
46
+ global.__pgPool__ = new import_pg.Pool({
47
+ host,
48
+ port,
49
+ user,
50
+ password,
51
+ database,
52
+ max: 10,
53
+ idleTimeoutMillis: 3e4,
54
+ connectionTimeoutMillis: 5e3
55
+ });
56
+ }
57
+ return global.__pgPool__;
58
+ };
59
+ var derivativesSchemaReady = false;
60
+ var spreadSchemaReady = false;
61
+ var toRows = (symbol, interval, data) => data.map((i) => ({
62
+ symbol,
63
+ interval,
64
+ ts: new Date(i.timestamp),
65
+ // ms -> Date
66
+ open: i.open,
67
+ high: i.high,
68
+ low: i.low,
69
+ close: i.close,
70
+ volume: i.volume ?? null,
71
+ turnover: i.turnover ?? null
72
+ }));
73
+ async function upsertCandles(rows) {
74
+ if (!rows.length) return;
75
+ const pool = getPool();
76
+ const cols = [
77
+ "symbol",
78
+ "interval",
79
+ "ts",
80
+ "open",
81
+ "high",
82
+ "low",
83
+ "close",
84
+ "volume",
85
+ "turnover"
86
+ ];
87
+ const maxRows = Math.floor(65535 / cols.length);
88
+ if (rows.length > maxRows) {
89
+ for (let i = 0; i < rows.length; i += maxRows) {
90
+ await upsertCandles(rows.slice(i, i + maxRows));
91
+ }
92
+ return;
93
+ }
94
+ const valuesSql = rows.map(
95
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
96
+ ).join(",");
97
+ const flat = rows.flatMap((r) => [
98
+ r.symbol,
99
+ r.interval,
100
+ r.ts,
101
+ r.open,
102
+ r.high,
103
+ r.low,
104
+ r.close,
105
+ r.volume ?? null,
106
+ r.turnover ?? null
107
+ ]);
108
+ const sql = `
109
+ INSERT INTO candles (${cols.join(",")})
110
+ VALUES ${valuesSql}
111
+ ON CONFLICT (symbol, interval, ts) DO UPDATE SET
112
+ open = EXCLUDED.open,
113
+ high = EXCLUDED.high,
114
+ low = EXCLUDED.low,
115
+ close = EXCLUDED.close,
116
+ volume = COALESCE(EXCLUDED.volume, candles.volume),
117
+ turnover = COALESCE(EXCLUDED.turnover, candles.turnover)
118
+ `;
119
+ const client = await pool.connect();
120
+ try {
121
+ await client.query("BEGIN");
122
+ await client.query(sql, flat);
123
+ await client.query("COMMIT");
124
+ } catch (e) {
125
+ await client.query("ROLLBACK");
126
+ throw e;
127
+ } finally {
128
+ client.release();
129
+ }
130
+ }
131
+ var ensureDerivativesSchema = async () => {
132
+ if (derivativesSchemaReady) return;
133
+ const pool = getPool();
134
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
135
+ await pool.query(`
136
+ CREATE TABLE IF NOT EXISTS derivatives_market (
137
+ symbol text NOT NULL,
138
+ interval text NOT NULL,
139
+ ts timestamptz NOT NULL,
140
+ open_interest double precision,
141
+ funding_rate double precision,
142
+ liq_long double precision,
143
+ liq_short double precision,
144
+ liq_total double precision,
145
+ source text,
146
+ ingested_at timestamptz NOT NULL DEFAULT now(),
147
+ PRIMARY KEY (symbol, interval, ts)
148
+ )
149
+ `);
150
+ await pool.query(`
151
+ SELECT create_hypertable(
152
+ 'derivatives_market',
153
+ 'ts',
154
+ if_not_exists => TRUE,
155
+ chunk_time_interval => interval '14 days'
156
+ )
157
+ `);
158
+ await pool.query(`
159
+ CREATE INDEX IF NOT EXISTS derivatives_market_symbol_tf_ts_idx
160
+ ON derivatives_market (symbol, interval, ts DESC)
161
+ `);
162
+ derivativesSchemaReady = true;
163
+ };
164
+ var ensureSpreadSchema = async () => {
165
+ if (spreadSchemaReady) return;
166
+ const pool = getPool();
167
+ await pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb");
168
+ await pool.query(`
169
+ CREATE TABLE IF NOT EXISTS market_spread (
170
+ symbol text NOT NULL,
171
+ interval text NOT NULL,
172
+ ts timestamptz NOT NULL,
173
+ binance_price double precision,
174
+ coinbase_price double precision,
175
+ spread double precision,
176
+ source text,
177
+ ingested_at timestamptz NOT NULL DEFAULT now(),
178
+ PRIMARY KEY (symbol, interval, ts)
179
+ )
180
+ `);
181
+ await pool.query(`
182
+ SELECT create_hypertable(
183
+ 'market_spread',
184
+ 'ts',
185
+ if_not_exists => TRUE,
186
+ chunk_time_interval => interval '14 days'
187
+ )
188
+ `);
189
+ await pool.query(`
190
+ CREATE INDEX IF NOT EXISTS market_spread_symbol_tf_ts_idx
191
+ ON market_spread (symbol, interval, ts DESC)
192
+ `);
193
+ spreadSchemaReady = true;
194
+ };
195
+ async function upsertDerivatives(rows) {
196
+ if (!rows.length) return;
197
+ await ensureDerivativesSchema();
198
+ const pool = getPool();
199
+ const cols = [
200
+ "symbol",
201
+ "interval",
202
+ "ts",
203
+ "open_interest",
204
+ "funding_rate",
205
+ "liq_long",
206
+ "liq_short",
207
+ "liq_total",
208
+ "source"
209
+ ];
210
+ const maxRows = Math.floor(65535 / cols.length);
211
+ if (rows.length > maxRows) {
212
+ for (let i = 0; i < rows.length; i += maxRows) {
213
+ await upsertDerivatives(rows.slice(i, i + maxRows));
214
+ }
215
+ return;
216
+ }
217
+ const valuesSql = rows.map(
218
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
219
+ ).join(",");
220
+ const flat = rows.flatMap((row) => [
221
+ row.symbol,
222
+ row.interval,
223
+ row.ts,
224
+ row.openInterest ?? null,
225
+ row.fundingRate ?? null,
226
+ row.liqLong ?? null,
227
+ row.liqShort ?? null,
228
+ row.liqTotal ?? null,
229
+ row.source ?? null
230
+ ]);
231
+ const sql = `
232
+ INSERT INTO derivatives_market (${cols.join(",")})
233
+ VALUES ${valuesSql}
234
+ ON CONFLICT (symbol, interval, ts) DO UPDATE SET
235
+ open_interest = COALESCE(EXCLUDED.open_interest, derivatives_market.open_interest),
236
+ funding_rate = COALESCE(EXCLUDED.funding_rate, derivatives_market.funding_rate),
237
+ liq_long = COALESCE(EXCLUDED.liq_long, derivatives_market.liq_long),
238
+ liq_short = COALESCE(EXCLUDED.liq_short, derivatives_market.liq_short),
239
+ liq_total = COALESCE(EXCLUDED.liq_total, derivatives_market.liq_total),
240
+ source = COALESCE(EXCLUDED.source, derivatives_market.source),
241
+ ingested_at = now()
242
+ `;
243
+ await pool.query(sql, flat);
244
+ }
245
+ async function getDerivativesRangeForSymbols(symbols, interval, startMs, endMs) {
246
+ if (!symbols.length)
247
+ return [];
248
+ await ensureDerivativesSchema();
249
+ const pool = getPool();
250
+ const sql = `
251
+ SELECT symbol, interval, ts, open_interest, funding_rate, liq_long, liq_short, liq_total
252
+ FROM derivatives_market
253
+ WHERE symbol = ANY($1)
254
+ AND interval = $2
255
+ AND ts >= to_timestamp($3/1000.0)
256
+ AND ts <= to_timestamp($4/1000.0)
257
+ ORDER BY symbol ASC, ts ASC
258
+ `;
259
+ const res = await pool.query(sql, [symbols, interval, startMs, endMs]);
260
+ return res.rows;
261
+ }
262
+ async function getDerivativesSummary(hours = 24, limit = 500) {
263
+ await ensureDerivativesSchema();
264
+ const pool = getPool();
265
+ const cappedHours = Math.max(1, Math.min(24 * 30, hours));
266
+ const cappedLimit = Math.max(50, Math.min(5e3, limit));
267
+ const rowsQ = await pool.query(
268
+ `
269
+ SELECT symbol, interval, ts, open_interest, funding_rate, liq_long, liq_short, liq_total
270
+ FROM derivatives_market
271
+ WHERE ts >= now() - ($1 || ' hours')::interval
272
+ ORDER BY ts DESC
273
+ LIMIT $2
274
+ `,
275
+ [String(cappedHours), cappedLimit]
276
+ );
277
+ const aggQ = await pool.query(
278
+ `
279
+ SELECT
280
+ symbol,
281
+ interval,
282
+ COUNT(*)::int AS points,
283
+ MAX(ts) AS last_ts,
284
+ AVG(open_interest) AS avg_open_interest,
285
+ AVG(funding_rate) AS avg_funding_rate,
286
+ SUM(COALESCE(liq_total, 0)) AS sum_liq_total
287
+ FROM derivatives_market
288
+ WHERE ts >= now() - ($1 || ' hours')::interval
289
+ GROUP BY symbol, interval
290
+ ORDER BY points DESC, symbol ASC
291
+ LIMIT 500
292
+ `,
293
+ [String(cappedHours)]
294
+ );
295
+ return {
296
+ rows: rowsQ.rows,
297
+ aggregates: aggQ.rows,
298
+ hours: cappedHours
299
+ };
300
+ }
301
+ async function upsertSpreadRows(rows) {
302
+ if (!rows.length) return;
303
+ await ensureSpreadSchema();
304
+ const pool = getPool();
305
+ const cols = [
306
+ "symbol",
307
+ "interval",
308
+ "ts",
309
+ "binance_price",
310
+ "coinbase_price",
311
+ "spread",
312
+ "source"
313
+ ];
314
+ const maxRows = Math.floor(65535 / cols.length);
315
+ if (rows.length > maxRows) {
316
+ for (let i = 0; i < rows.length; i += maxRows) {
317
+ await upsertSpreadRows(rows.slice(i, i + maxRows));
318
+ }
319
+ return;
320
+ }
321
+ const valuesSql = rows.map(
322
+ (_, i) => `(${cols.map((__, j) => `$${i * cols.length + j + 1}`).join(",")})`
323
+ ).join(",");
324
+ const flat = rows.flatMap((row) => [
325
+ row.symbol,
326
+ row.interval,
327
+ row.ts,
328
+ row.binancePrice ?? null,
329
+ row.coinbasePrice ?? null,
330
+ row.spread ?? null,
331
+ row.source ?? null
332
+ ]);
333
+ const sql = `
334
+ INSERT INTO market_spread (${cols.join(",")})
335
+ VALUES ${valuesSql}
336
+ ON CONFLICT (symbol, interval, ts) DO UPDATE SET
337
+ binance_price = COALESCE(EXCLUDED.binance_price, market_spread.binance_price),
338
+ coinbase_price = COALESCE(EXCLUDED.coinbase_price, market_spread.coinbase_price),
339
+ spread = COALESCE(EXCLUDED.spread, market_spread.spread),
340
+ source = COALESCE(EXCLUDED.source, market_spread.source),
341
+ ingested_at = now()
342
+ `;
343
+ await pool.query(sql, flat);
344
+ }
345
+ async function getSpreadRangeForSymbols(symbols, interval, startMs, endMs) {
346
+ if (!symbols.length) {
347
+ return [];
348
+ }
349
+ await ensureSpreadSchema();
350
+ const pool = getPool();
351
+ const sql = `
352
+ SELECT symbol, interval, ts, binance_price, coinbase_price, spread
353
+ FROM market_spread
354
+ WHERE symbol = ANY($1)
355
+ AND interval = $2
356
+ AND ts >= to_timestamp($3/1000.0)
357
+ AND ts <= to_timestamp($4/1000.0)
358
+ ORDER BY symbol ASC, ts ASC
359
+ `;
360
+ const res = await pool.query(sql, [symbols, interval, startMs, endMs]);
361
+ return res.rows;
362
+ }
363
+ async function getSpreadSummary(hours = 24, limit = 500) {
364
+ await ensureSpreadSchema();
365
+ const pool = getPool();
366
+ const cappedHours = Math.max(1, Math.min(24 * 30, hours));
367
+ const cappedLimit = Math.max(50, Math.min(5e3, limit));
368
+ const rowsQ = await pool.query(
369
+ `
370
+ SELECT symbol, interval, ts, binance_price, coinbase_price, spread
371
+ FROM market_spread
372
+ WHERE ts >= now() - ($1 || ' hours')::interval
373
+ ORDER BY ts DESC
374
+ LIMIT $2
375
+ `,
376
+ [String(cappedHours), cappedLimit]
377
+ );
378
+ const aggQ = await pool.query(
379
+ `
380
+ SELECT
381
+ symbol,
382
+ interval,
383
+ COUNT(*)::int AS points,
384
+ MAX(ts) AS last_ts,
385
+ AVG(spread) AS avg_spread,
386
+ STDDEV_POP(spread) AS std_spread
387
+ FROM market_spread
388
+ WHERE ts >= now() - ($1 || ' hours')::interval
389
+ GROUP BY symbol, interval
390
+ ORDER BY points DESC, symbol ASC
391
+ LIMIT 500
392
+ `,
393
+ [String(cappedHours)]
394
+ );
395
+ return {
396
+ rows: rowsQ.rows,
397
+ aggregates: aggQ.rows,
398
+ hours: cappedHours
399
+ };
400
+ }
401
+ async function getCandlesRange(symbol, interval, startMs, endMs) {
402
+ const pool = getPool();
403
+ const sql = `
404
+ SELECT symbol, interval, ts,
405
+ open, high, low, close, volume, turnover
406
+ FROM candles
407
+ WHERE symbol = $1 AND interval = $2
408
+ AND ts >= to_timestamp($3/1000.0)
409
+ AND ts <= to_timestamp($4/1000.0)
410
+ ORDER BY ts ASC
411
+ `;
412
+ const res = await pool.query(sql, [symbol, interval, startMs, endMs]);
413
+ return res.rows;
414
+ }
415
+ async function getDataEdges(symbol, interval) {
416
+ const pool = getPool();
417
+ const sqlMin = `
418
+ SELECT extract(epoch from ts)*1000 AS ms
419
+ FROM candles
420
+ WHERE symbol=$1 AND interval=$2
421
+ ORDER BY ts ASC
422
+ LIMIT 1
423
+ `;
424
+ const sqlMax = `
425
+ SELECT extract(epoch from ts)*1000 AS ms
426
+ FROM candles
427
+ WHERE symbol=$1 AND interval=$2
428
+ ORDER BY ts DESC
429
+ LIMIT 1
430
+ `;
431
+ const [minQ, maxQ] = await Promise.all([
432
+ pool.query(sqlMin, [symbol, interval]),
433
+ pool.query(sqlMax, [symbol, interval])
434
+ ]);
435
+ const minRaw = minQ.rows[0]?.ms;
436
+ const maxRaw = maxQ.rows[0]?.ms;
437
+ const min = Number.isFinite(Number(minRaw)) ? Number(minRaw) : void 0;
438
+ const max = Number.isFinite(Number(maxRaw)) ? Number(maxRaw) : void 0;
439
+ return { min, max };
440
+ }
441
+ async function waitForDbReady(attempts = 20, delayMs = 1e3) {
442
+ const pool = getPool();
443
+ let lastError;
444
+ for (let i = 0; i < attempts; i++) {
445
+ try {
446
+ await pool.query("SELECT 1");
447
+ return;
448
+ } catch (e) {
449
+ lastError = e;
450
+ await new Promise((r) => setTimeout(r, delayMs));
451
+ }
452
+ }
453
+ throw lastError;
454
+ }
455
+ async function deleteCandles(symbol, interval) {
456
+ const pool = getPool();
457
+ const sql = `
458
+ DELETE FROM candles
459
+ WHERE symbol = $1 AND interval = $2
460
+ `;
461
+ await pool.query(sql, [symbol, interval]);
462
+ }
463
+ async function findContinuityGap(symbol, interval) {
464
+ const pool = getPool();
465
+ const expectedSeconds = interval * 60;
466
+ const sql = `
467
+ WITH ordered AS (
468
+ SELECT
469
+ ts,
470
+ LAG(ts) OVER (ORDER BY ts) AS prev_ts
471
+ FROM candles
472
+ WHERE symbol = $1 AND interval = $2
473
+ )
474
+ SELECT
475
+ ts,
476
+ prev_ts,
477
+ EXTRACT(EPOCH FROM (ts - prev_ts))::int AS diff_seconds
478
+ FROM ordered
479
+ WHERE prev_ts IS NOT NULL
480
+ AND EXTRACT(EPOCH FROM (ts - prev_ts))::int <> $3
481
+ ORDER BY ts ASC
482
+ LIMIT 1
483
+ `;
484
+ const res = await pool.query(sql, [symbol, interval, expectedSeconds]);
485
+ const row = res.rows[0];
486
+ if (!row) return null;
487
+ return {
488
+ ts: new Date(row.ts).getTime(),
489
+ prevTs: new Date(row.prev_ts).getTime(),
490
+ diffSeconds: row.diff_seconds
491
+ };
492
+ }
493
+ // Annotate the CommonJS export names for ESM import in node:
494
+ 0 && (module.exports = {
495
+ deleteCandles,
496
+ findContinuityGap,
497
+ getCandlesRange,
498
+ getDataEdges,
499
+ getDerivativesRangeForSymbols,
500
+ getDerivativesSummary,
501
+ getSpreadRangeForSymbols,
502
+ getSpreadSummary,
503
+ toRows,
504
+ upsertCandles,
505
+ upsertDerivatives,
506
+ upsertSpreadRows,
507
+ waitForDbReady
508
+ });