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